pax_global_header00006660000000000000000000000064130565515070014521gustar00rootroot0000000000000052 comment=27ae86b6608aaabf354b1432e85dfb1ebf964569 qgit-2.7/000077500000000000000000000000001305655150700123355ustar00rootroot00000000000000qgit-2.7/.gitignore000066400000000000000000000002711305655150700143250ustar00rootroot00000000000000/.kateproject.d/notes.txt /.kateproject /.directory Makefile* *~ bin build *kde* Doxy* qgit4.tag Debug/ Release/ *.ncb *.suo *.vcproj.* moc_*.cpp qrc_*.cpp ui_*.h qgit.pro.* qgit.qbs.* qgit-2.7/.travis.yml000066400000000000000000000007151305655150700144510ustar00rootroot00000000000000sudo: required dist: trusty language: cpp compiler: - gcc - clang env: matrix: - META_MAKE="cmake" - META_MAKE="qmake" - META_MAKE="qmake-qt4" install: # Use this to install any prerequisites or dependencies necessary to run the build - sudo apt-get -qq install qt5-default libqt4-dev qt4-qmake before_script: # build - mkdir build; cd build - $META_MAKE .. - make - sudo make install script: # run tests - echo "No tests yet."qgit-2.7/CMakeLists.txt000066400000000000000000000055751305655150700151110ustar00rootroot00000000000000# # see http://qt-project.org/doc/qt-5/cmake-manual.html # see also http://www.kdab.com/using-cmake-with-qt-5/ # see also http://stackoverflow.com/questions/16245147/unable-to-include-a-ui-form-header-of-qt5-in-cmake # see also http://www.qtcentre.org/wiki/index.php?title=Compiling_Qt4_apps_with_CMake # cmake_minimum_required(VERSION 2.8.11) project(qgit) include(GNUInstallDirs) # As moc files are generated in the binary dir, tell CMake # to always look for includes there: set(CMAKE_INCLUDE_CURRENT_DIR ON) # Instruct CMake to run moc automatically when needed. set(CMAKE_AUTOMOC ON) option(UseQt5 "Use Qt5?" ON) if (UseQt5) find_package(Qt5 REQUIRED COMPONENTS Core Widgets) set(QT_LIBRARIES Qt5::Widgets) macro(qt_wrap_ui) qt5_wrap_ui(${ARGN}) endmacro() macro(qt_add_resources) qt5_add_resources(${ARGN}) endmacro() else() find_package(Qt4 REQUIRED COMPONENTS QtCore QtGui) include(${QT_USE_FILE}) macro(qt_wrap_ui) qt4_wrap_ui(${ARGN}) endmacro() macro(qt_add_resources) qt4_add_resources(${ARGN}) endmacro() endif() include_directories( ${CMAKE_SOURCE_DIR}/src ) set(CPP_SOURCES src/annotate.cpp src/cache.cpp src/commitimpl.cpp src/common.cpp src/consoleimpl.cpp src/customactionimpl.cpp src/dataloader.cpp src/domain.cpp src/exceptionmanager.cpp src/filecontent.cpp src/FileHistory.cc src/filelist.cpp src/fileview.cpp src/git.cpp src/lanes.cpp src/listview.cpp src/inputdialog.cpp src/mainimpl.cpp src/myprocess.cpp src/namespace_def.cpp src/patchcontent.cpp src/patchview.cpp src/qgit.cpp src/rangeselectimpl.cpp src/revdesc.cpp src/revsview.cpp src/settingsimpl.cpp src/smartbrowse.cpp src/treeview.cpp ) # UIS_HDRS will be used later in add_executable QT_WRAP_UI(UIS_HDRS src/commit.ui src/console.ui src/customaction.ui src/fileview.ui src/help.ui src/mainview.ui src/patchview.ui src/rangeselect.ui src/revsview.ui src/settings.ui ) # and finally an resource file SET(RESOURCE_FILES src/icons.qrc ) # this command will generate rules that will run rcc on all files from SAMPLE_RCS # in result SAMPLE_RC_SRCS variable will contain paths to files produced by rcc QT_ADD_RESOURCES(RC_SRCS ${RESOURCE_FILES}) add_executable(qgit ${CPP_SOURCES} ${UIS_HDRS} ${RC_SRCS}) target_link_libraries(qgit ${QT_LIBRARIES}) install(TARGETS qgit DESTINATION bin) if (UNIX) install(FILES src/resources/qgit.png DESTINATION ${CMAKE_INSTALL_DATAROOTDIR}/icons/hicolor/48x48/apps) install(FILES qgit.desktop DESTINATION ${CMAKE_INSTALL_DATAROOTDIR}/applications) install(FILES qgit.appdata.xml DESTINATION ${CMAKE_INSTALL_DATAROOTDIR}/metainfo) endif() # kate: indent-width 4; replace-tabs on; # notes: # http://stackoverflow.com/questions/15054117/aligning-qgraphicsitems-to-a-grid-when-dragging-and-dropping qgit-2.7/COPYING.rtf000066400000000000000000000400361305655150700141650ustar00rootroot00000000000000{\rtf1\ansi\ansicpg1250\deff0\deflang1033\deflangfe1060{\fonttbl{\f0\fswiss\fprq2\fcharset238 Verdana;}{\f1\fmodern\fprq1\fcharset238 Lucida Console;}} {\*\generator Msftedit 5.41.15.1507;}\viewkind4\uc1\pard\nowidctlpar\sb100\sa100\qc\lang1060\kerning36\b\f0\fs28 GNU General Public License\par \kerning0\b0\fs16 Version 2, June 1991\par \pard\nowidctlpar\f1\fs14\par Copyright (C) 1989, 1991 Free Software Foundation, Inc.\par 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA\par \par Everyone is permitted to copy and distribute verbatim copies\par of this license document, but changing it is not allowed.\par \par \pard\keepn\nowidctlpar\sb100\sa100\qc\b\f0\fs20 Preamble\fs24\par \pard\nowidctlpar\fi142\sb100\sa100\b0\fs16 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 Library General Public License instead.) You can apply it to your programs, too. \par 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. \par 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. \par 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. \par 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. \par 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. \par 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. \par The precise terms and conditions for copying, distribution and modification follow. \par \pard\keepn\nowidctlpar\sb100\sa100\qc\b\fs20 TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION\par \pard\nowidctlpar\fi142\sb100\sa100\fs16 0.\b0 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". \par \pard\nowidctlpar\sb100\sa100 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. \par \pard\nowidctlpar\fi142\sb100\sa100\b 1.\b0 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. \par \pard\nowidctlpar\sb100\sa100 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. \par \pard\nowidctlpar\fi142\sb100\sa100\b 2.\b0 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: \par \pard\nowidctlpar\li284\sb100\sa100\b a)\b0 You must cause the modified files to carry prominent notices stating that you changed the files and the date of any change. \par \b b)\b0 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. \par \b c)\b0 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.) \par \pard\nowidctlpar\sb100\sa100 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. \par 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. \par 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. \par \pard\nowidctlpar\fi142\sb100\sa100\b 3.\b0 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: \v \v0\par \pard\nowidctlpar\li284\sb100\sa100\b a)\b0 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, \par \b b)\b0 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, \par \b c)\b0 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.) \par \pard\nowidctlpar\sb100\sa100 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. \par 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. \par \pard\nowidctlpar\fi142\sb100\sa100\b 4.\b0 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. \par \b 5.\b0 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. \par \b 6.\b0 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. \par \b 7.\b0 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. \par \pard\nowidctlpar\sb100\sa100 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. \par 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. \par This section is intended to make thoroughly clear what is believed to be a consequence of the rest of this License. \par \pard\nowidctlpar\fi142\sb100\sa100\b 8.\b0 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. \par \b 9.\b0 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. \par \pard\nowidctlpar\sb100\sa100 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. \par \pard\nowidctlpar\fi142\sb100\sa100\b 10.\b0 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. \par \pard\nowidctlpar\sb100\sa100\qc\fs20 NO WARRANTY\par \pard\nowidctlpar\fi142\sb100\sa100\b\fs16 11.\b0 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. \par \b 12.\b0 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. \par \pard\keepn\nowidctlpar\sb100\sa100\qc\b END OF TERMS AND CONDITIONS\fs20\par } \ No newline at end of file qgit-2.7/README000066400000000000000000000370571305655150700132310ustar00rootroot00000000000000qgit, a git GUI viewer ====================== With qgit you will be able to browse revisions history, view patch content and changed files, graphically following different development branches. Bugs or feature requests should be sent to the git@vger.kernel.org mailing list. Main features ------------- - View revisions, diffs, files history, files annotation, archive tree. - Commit changes visually cherry picking modified files. - Apply or save patch series from selected commits, drag and drop commits between two instances of qgit. - Associate commands sequences, scripts and anything else executable to a custom action. Actions can be run from menu and corresponding output is grabbed by a terminal window. - qgit implements a GUI for the most common StGIT commands like push/pop and apply/save patches. You can also create new patches or refresh current top one using the same semantics of git commit, i.e. cherry picking single modified files. Installation ------------ You need Qt developer libraries, version 4.3 or later, already installed. Be sure compiler, qmake (Qt4 version) and Qt4 bin tools are in path. NOTE: Correct qmake for Qt4 (/usr/lib/qt4/bin/qmake) must be called, in case also Qt3 version is in path the former must be explicitly invoked. To compile, install and start qgit: - unpack the released tar file or clone from a git public archive - (Windows only) open src/src.pro and set the proper GIT_EXEC_DIR value - generate Makefiles (only the first time) Unix/Linux and Windows: qmake qgit.pro Mac Os X: qmake -spec macx-g++ qgit.pro - (Windows only with VC2008 IDE) open qgit.sln solution file - make - make install - (Windows only) run start_qgit.bat to start the application. You can also create a a desktop icon linked to 'start_qgit.bat' and double click on it. Installation directory is $HOME/bin under Linux and the directory of git exe files under Windows. You need to run 'qmake qgit.pro' only the first time to generate Makefile files, then you simply call 'make' and 'make install'. You may need to run 'qmake qgit.pro' again only after patches that modify 'qgit.pro' or 'src/src.pro' files or in case of 'strange' compile errors. Remember to manually delete all Makefile* files in 'src/' directory before to start 'qmake qgit.pro'. Performance tweaks ------------------ A 'git log' command is used to load the repository at startup or when refreshing. This is an highly performance critical task. Default method is based on a temporary file as data exchange facility. This is the fastest on Linux with a tmpfs filesystem mounted under /tmp, as is common with most distributions. In case of portability issues it is possible to fallback on a standard QProcess based interface. To do this uncomment USE_QPROCESS define in 'src/dataloader.h' before to compile. Command line arguments ---------------------- Run qgit from a git working directory, command line arguments are filtered by 'git log'. Some examples: qgit --no-merges qgit v2.6.18.. include/scsi drivers/scsi qgit --since="2 weeks ago" -- kernel/ qgit -r --name-status release..test If qgit is launched without arguments or if you change archive with 'open' menu, a dialog for range select is shown. You can select top and bottom rev tags from the list or paste a specific revision. Values are passed to 'git log' to narrow data loading to chosen revisions. Main view --------- You can navigate through logs, file names, file history, archive tree. All the views will be updated accordingly. Copy/paste is supported on all fields. Copy (CTRL+C) is supported on all views. All the references found recursively under .git/refs/ directory are highlighted according to their type: current branch(HEAD), branch, tag, other. Reference names and any associated messages can be viewed in status bar when a tagged revision is selected. When you right click on main view a context sensitive pop-up menu is shows available commands and a 'quick jump' tag list. *Key bindings*:: `-------------`---------------------------------- r Go to revisions list page p Go to patch page f Go to file page Go to next/previous page t Toggle tree view s Toggle view of secondary panes h Toggle view of revision header Move to first revision Move to last revision i Move up one revision in main view (global scope) n, k Move down one revision in main view (global scope) Move to previous highlighted line Move to next highlighted line Go back in history list Go forward in history list Increase font size Decrease font size , b Scroll content up one page Scroll content up one page Scroll content down one page u Scroll content up 18 lines d Scroll content down 18 lines ------------------------------------------------- *Directory tree*:: From menu or toolbar button it is possible to show a side panel with tree view of repository files and directories. + Double clicking on a file opens file annotation window. With 'filter by tree' button it is possible to compress revision list to show only selected files/directories in tree view. + Tree view supports multi-selection. When you right click on a file on tree view a context sensitive pop-up menu is shows with available commands. *Working directory changes*:: When 'Check working dir' flag is set, as example from main view context pop-up menu, a pseudo-revision is shown and highlighted at the top of the list. Highlight and revision name reflect current working directory status: 'Nothing to commit' or 'Working directory changes' respectively. + To check for working directory modified files set corresponding preference in Edit->Settings->'Working dir'. QGit checks for possible new files added in working directory using ignoring rules according to git ls-files specifications, see menu Edit->Settings->'Working dir'. + TIP: If you don't need to see modified files in working dir, disable corresponding setting and start-up time will be shorter. *Lane info*:: Selecting a lane with mouse right button will display a pop-up with the list of children and parent. Select one and you jump to it. *Filter / Highlight*:: Use the combo box to select where you want to filter or highlight on. Currently supported fields are: log header, log message, revision author, revision SHA1, file name and patch content. + Write a filter string, press filter button and the view will update showing only commits that contain the filter string, case insensitive. Toggle filter button to release the filter. + Alternatively press the magnifying glass button, in this case matched lines will be highlighted, you can use and keys to browse them. Toggle the button to remove the highlighting. + NOTE: In case of patch content regexp filtering, the given string is interpreted as a POSIX regular expression, not as a simple substring. + TIP: Very useful to quick retrieve a sha writing only first 3-4 digits and filtering / highlighting on revision sha. The sha value can then be copied from SHA field. + TIP: It is possible to insert an abbreviated sha directly in the SHA line edit at the top right of the window. After pressing enter this will trigger an higlighting of the matched revisions. It is a kind of shortcut of the previous tip. *Save patch series*:: After mouse selecting the chosen revisions (use standard CTRL+left click) for single select or SHIFT+left click for range select), press 'Save Patch' button or use file menu and a dialog will let you choose patches destination directory. Then 'git format-patch-script' will be called and patches created. It is possible to specify additional options with Edit->Settings menu. *Apply patch*:: This menu entry is complementary to save patch and it's an interface to 'git am'. *Drag and drop*:: It is possible to drag some selected revs from one instance of qgit to another open on a different archive. In this case 'git format-patch' is used in the dragging archive to create temporary patches imported in the dropping archive by 'git am'. *Make branch*:: Select a revision and open Edit->'Make Branch' or use right click context pop-up menu. A dialog will be shown asking for a branch name. *Make tag*:: Select a revision and open Edit->'Make Tag' or use right click context pop-up menu. Two dialogs will be shown, the first asking for a tag name, the second for a tag message (not mandatory). If a non empty message is written, this will be saved together with the tag. Tags and tag messages can be viewed in status bar when a tagged revision is selected. *Delete tag*:: Select a tagged revision and open Edit->'Delete Tag' or use right click context pop-up menu. After confirmation the selected revision will be untagged. *Save file*:: Select a file from tree or file list and open File->'Save file as' or use the tree view context sensitive pop-up menu (right click), a dialog will be shown asking for a file name (default to current) and destination directory. Input a valid name, press OK and the file will be saved. *Commit changes*:: When enabled with Edit->Settings->'Working dir'->'Diff against working dir' and there is something committable, a special highlighted first revision is shown, with the status of the archive and the possible pending stuff. From Edit->Commit it is then possible to invoke the commit dialog. + In commit dialog select the files to commit or, simply, to sync with index (call 'git update-index' on them). A proper commit message may be entered and, after confirmation, changes are committed and a new revision is created. + It is also possible to amend last commit. The Edit->Amend commit opens the same dialog, but changes are added to the head commit instead of creating new commit. + The core commit function is performed by 'git commit'. + TIP: It is possible to use a template for commit message, use Edit->Settings->Commit to define template file path. Patch viewer ------------ To open patch tab use context menu, double click on a revision or file in main view or select View->'View patch' menu (CTRL+P). The patch shown is the diff of current selected commit against: - Parent (default) - HEAD - Selected SHA or reference name In the last case SHA is chosen by writing or pasting a tree-ish or a reference names in the corresponding field and pressing return. You get the same result also with a CTRL+right click on a revision in main list. Selected target will be highlighted. CTRL+right click again on the highlighted revision to release the filter. With the 'filter' button at the right of the tool bar it is possible to toggle the display of removed code lines. This can be useful to easy reading of the patch. External diff tool -------------------- From 'View->External diff' it is possible to invoke an external diff tool, as example to view the diffs in a two vertical tiled windows. External diff tool shows the diffs between two files. First file is the current selected file of current revision. Second file is the same file of the parent revision or of a specific revision if 'diff to sha' feature is enabled (diff target is highlighted, see above). Default external viewer is kompare, but it is possible to set a preferred one from 'Edit->Settings->External Diff Tool'. File viewer ----------- It is possible to view file contents of any file at any revision time in history. *File list panel*:: In the bottom right of main view a list of files modified by current revision is shown. Selecting a file name will update the patch view to center on the file. File names colors use the following convention - black for modified files - green for new files - red for removed files - dark blue for renamed/copied files *Merge files*:: In case of merges the groups of files corresponding to each merge parent are separated by two empty lines. + In case of merges you can chose between to see all the merge files or only the interesting ones (default), i.e. the files modified by more then one merge parent. *File content*:: To view file content double click on a file name in tree view, or use context menu in file list or select View->'View file' menu (CTRL+A). + In file view page will be shown current revision's file content and file history. + It is possible to copy to the clipboard the selected content with CTRL+C or with the corresponding button. *File annotations*:: On opening or updating file viewer, file history will be retrieved from archive together with file content. Annotations are then calculated in background and the view is updated when ready. + Double clicking on an annotation index number will update history list to point to corresponding revision. + Hovering the mouse over an annotation index will show a tool tip with the corresponding revision description. + File content will change too, to show new selected revision file. To keep the same view content after double clicking, probably what you want, just pin it with 'Pin view' check button. Next to the check button there is a spinbox to show/select the current revision number. + Double click on history list entry to update main, patch and tree views to corresponding revision. *Code region filter*:: When annotation info is available the 'filter' button is enabled and it is possible to mouse select a region of file content. Then, when pressing the filter button, only revisions that modify the selected region will be visible. Selected code region is highlighted and a shrunken history is shown. Filter button is a toggle button, so just press it again to release the filter. *Syntax highlighter*:: If GNU Source-highlight (http://www.gnu.org/software/src-highlite/) is installed and in PATH then it is possible to toggle source code highlight pressing the 'Color text' tool button. Please refer to Source-highlight site for the list of supported languages and additional documentation. Actions ------ Actions can be added/removed using a dedicated dialog invoked from 'Actions->Setup actions...' menu. Actions can be activated clicking on their name from the Actions menu. Each action can be associated to a list of any type of git or shell commands or to an external script. While an action is running a terminal window is shown to display the corresponding output. An action can also ask for command line arguments before to run so to allow for maximum flexibility. NOTE: command line arguments are always appended to the first command only. This lets you define an action like: git fetch git merge And if you type 'origin' when prompted, the action executed will be: git fetch origin git merge If you need a more complex arguments passing with a shell like notation define a script and associate your action to it. Integration with StGIT ---------------------- When a StGIT stack is found on top of a git archive, qgit transparently handles the added information. Integration with StGIT is implemented both by new and modified functions. .New functions are automatically activated: - Visualization of applied and unapplied patches in main view. - Interface to push/pop patches by a mouse right click on selected items. Push supports also multi-selection. .Existing functions change behavior: - Amend commit dialog refreshes top stack patch with modified files instead of amending the commit. It is appropriately renamed in the menu. - Commit dialog creates a new patch on the top of the stack filled with modified working directory content instead of commit a new revision to git repository. - Apply patch changes to interface StGIT import and fold commands instead of applying patch directly on the git repository. qgit-2.7/README_WIN.txt000066400000000000000000000017521305655150700145550ustar00rootroot00000000000000This is qgit.exe statically linked to Trolltech Qt4.3.3 for Windows Open source edition PREREQUISITES - You need msysgit (http://code.google.com/p/msysgit/) correctly installed NOTES - This version has NOT been tested with cygwin version of git - In case qgit has problem to find correct msysgit files you should add mysysgit\bin directory to your PATH before to start qgit. But normally installer should had already take care of this for you. WINDOWS DEVELOPER INFO This version has been tested with Visual C++ 2008, there is a VC2008 solution and a project file for your convenience. Please note, the FIRST TIME you need to generate a Makefile. To do this the Qt qmake tool must be used. Open a console on the project directory and type (qmake should be in PATH) qmake qgit.pro That's all, now you can fire your VC2008 IDE, open the qgit solution file and enjoy ;-) Please report any problem to the Git Mailing List specifying [QGit] in your mail subject qgit-2.7/TODO.txt000066400000000000000000000047541305655150700136550ustar00rootroot000000000000002011-10-22 No more distinction between revision and patch view. Make view switch between review / half-half / patch. 2011-10-22 Ctrl-R -> R, Ctrl-P -> P, Ctrl-A -> F 2011-10-22 Patch navigation (per difference, per file) from keyboard 2011-10-22 Headers always on top. 2011-10-22 Fix scrolling of patch going under headers 2011-10-22 WISH Add an "all" option to "Branches" tree 2011-10-22 WISH Add source highlighting based on kate/qtextviewer. 2011-10-23 Size of splits automatically computed. 2011-10-23 regression wrt not marking sha in patch (diff) 2011-10-23 regression wrt what the diff shows 2011-10-23 WISH add side by side diff view 2011-10-23 Regression files not shown in tree at first display (must hide/unhide) 2011-10-23 Regression file selection doesn't show file diff anymore 2011-10-23 WISH hyper-tooltips on revision (containing what is displayed in headers today) 2011-10-25 commit message colorization a la vim in commit.ui 2011-10-25 beautify console.ui for custom actions 2011-10-25 make customaction a table with checkbuttons indicated next to each command 2011-10-25 promote documentation to docbook or at least html 2011-10-25 respect current style's tab appearence (make quit buttons less ugly) 2011-10-25 make range select a tab in the sidebar (with icons marking top and bottom in the branches/tags list/tree) 2011-10-25 make .svg qgit icon for installation (make installation in kde automatic) 2011-10-25 a small toolbar above file list allows showing "diff selector" (check) and "message/diff display" (push) - selecting "diff selector" switches to "diff display" - selecting file switches to "diff display" - selecting "message display" unselects all files (memorizes last selected) and shows message 2011-10-25 "R" shows revisions, "P" shows patches, "F" shows files, "M" shows message, "D" shows diff, "S" shows R+P (split) "B" toggles branches "T" toggles files 2011-11-24 separate diff, diff HEAD and diff --cached in revisions view 2012-09-30 turn initMimePixMap, freeMimePixMap, mimePixMap and mimePix into a Singleton (gc plus opens way for connecting to underlaying OS mimetype management) 2012-09-30 clarify repo for startup: current dir, parameter from CLI, last in history etc. 2012-09-30 make refreshRepo automatic (monitor files in .git/ and wd?), make refreshRepo unavail during refresh (doh) (see first comment in MainImpl::setRepository()) qgit-2.7/exception_manager.txt000066400000000000000000000301521305655150700165670ustar00rootroot00000000000000 THE PROBLEM Qt3 uses the call qApp->processEvents() (from now on PE) to let a program do not block while a long operation is occurring. This function passes the control to the application event loop and any new pending event like GUI event, socket notifier(QProcess) and others are processed. The problem with PE is that when the calling function returns a changed context could be found. Some objects could be born, other disappeared, some action incompatible with the previous context could have been done, as example closing a window while processing or clearing a data container that the calling function is using. How to deal with this? A way is to check some 'context flags' on return from PE to search for a 'wrong context' and take appropriate actions. But this is not a good general solution because implies that the function that calls, and returns, from PE knows about the whole context. As example, A() is a function that access some data (kept in any kind of container a list, a file, etc), B() is a function that makes a lengthy operation and uses PE. If we have a code flow like the following A() |---> B() | |-----> qApp->processEvents() | |-----> do something else | return |---> .... |---> data.access() We should check inside of B() if the data is no more valid and eventually return an error code. But B() knows nothing about the our data. Of course we should check in A(), just after the B() call, but this is sub-optimal because it implies that we know for sure that B() does not accesses the data _nor_ any other function called by B() before to return to A(). In real software the call chain between the function that uses a resource and the function that pass the control to the application event loop and return from it can be very long and complex. INTRODUCING EXCEPTIONS The standard way that C++ has to deal with this kind of problems is called exceptions handling. If B() throws an exception, returning from PE, on a corrupted database, and the first catch clause is in A(), no matter how long and complex the chain is, A() will catch the exception before anything else is done. This seems interesting, but has two drawbacks, one general, one depending on Qt. -Exception resuming/termination models What happens if B() is called on start up, on data refreshing, or, in general, in a context where database consistency is not important or, worst, should not be checked? The exception will be thrown, no catch clause will take the exception, the default handler will be invoked and this means, at least in C++, program termination [in C++ you can provide a custom set_terminate() function to handle uncaught exceptions but you cannot continue from here, just clean-up and exit]. The general problem has a name and is called 'C++ supports termination-model exceptions, not resumption'. So we need a way to let B() throw an exception _only_ if the exception will be caught, IE only if the abnormal condition is 'interesting' in the actual context. -Exceptions trough signals/slots Standard C++ try-throw-catch exception system is not compatible with Qt signals/slots. If a function B() is called by A() by means of a signal the catch clause will not work, also if signals/slots in Qt3 are wrappers to direct calls. A() |---> try |---> emit mySignal --> slotMySignal() | |-----> throw X |---> catch(x) | |---> we will NOT catch X It is possible to code to avoid signals/slots when exceptions are involved, but the _real_ problem is that also PE is a barrier to exceptions propagation. What happens is the following: A() |---> try | B() | |-----> qApp->processEvents() | | |----> C() | | |----> database.clear() | | throw databaseIsEmpty | |<----- qApp->processEvents() | | | <----return | |---> catch(databaseIsEmpty) | | | |---> we will NOT catch databaseIsEmpty This is very unfortunate. INTRODUCING EXCEPTION MANAGER If we rewrite the above scheme as follows: A() |---> try | B() | |-----> qApp->processEvents() | | |----> C() | | |----> database.clear() | | throw databaseIsEmpty | | | | |<----- qApp->processEvents()<----return | | | if (databaseIsEmpty is throwable) | throw databaseIsEmpty | <----return | |---> catch(databaseIsEmpty) | | | |---> NOW we will catch databaseIsEmpty Two things have changed between the schemes. - The potential exception is checked to verify if it is among the throwables exceptions - The exception is thrown in the same region* of the catch clause *[A 'region' is the code that executes between two calls of PE] Class ExceptionManager does exactly this, checks the exception against a throwable set, wait until the correct region is reached and finally throws the exception. If we rewrite the above code to use ExceptionManager helper macros we have: A() { ..... try { EM_REGISTER(databaseIsEmpty); // adds databaseIsEmpty to the throwable set ..... B(); ..... EM_REMOVE(databaseIsEmpty); // removes databaseIsEmpty from the throwable set } catch (int i) { EM_REMOVE(databaseIsEmpty); if (i == databaseIsEmpty) { .....handle the exception.... EM_CHECK_PENDING; // re-check any other pending exception } } ..... } B() { ..... EM_BEFORE_PROCESS_EVENTS; // some magic occurs ;-) while(something_happens) qApp->processEvents(); EM_AFTER_PROCESS_EVENTS; // throws the pending exceptions belonging to the current region ..... } C() { ..... database.clear(); EM_RAISE(databaseIsEmpty); // checks if databaseIsEmpty is throwable and, in case, ..... // flags it as 'raised'. In the latter case it will be ..... // thrown, but only when returning in the correct region. } With this scheme everything works as expected. There are some things to note: 1) In B() there is no knowledge of 'databaseIsEmpty'. B() does not have to know about the general context at all. 2) At the end of the catch clause any other pending exception will be thrown, so to allow for multiple raised exceptions. 3) The same exception will be thrown as many times as has been registered. What it means is ExceptionManager supports nested try-catch blocks, also when looking for the same exception, in this case each catch clause will be called with the same exception and in the correct order. 4) ExceptionManager has an internal multi-region stack. What it means is that try-catch blocks can be nested _across_ many PE calls: each catch clause will be called with the correct raised exception and in the correct time, when returning in the corresponding region from a PE call. No matter when the exceptions have been raised. TECHNICAL DETAILS A 'region' is the code that executes between two calls of qApp->processEvents() the code in a region has the stack property. Ie if fb() is called inside fa() then fb() will return before fa(). A 'catch set' is a set of exceptions that are added at the beginning of the same try block. Given a group of exceptions of the same catch set the following can occur: 1- No exception is raised -> the catch clause is not called. 2- Only one exception Ex of the set is raised -> the catch clause is called with Ex parameter. 3- More then one exception Ex1, Ex2,..Exn are raised -> the catch clause is called with Ex1 parameter, ie the first priority exception. The exception priority is given when adding the exceptions at the beginning of try block. The last added is the first priority. The totalThrowableSet is a list of exceptions that can be raised with a call to raise(excp). The regionThrowableSet is a subset of totalThrowableSet and lists the exceptions that can be thrown in the corresponding region. The regionThrowableSet is saved before to call qApp->processEvents() and restored on return. A call to qApp->processEvents() trigger a region boundary. So a new and empty regionThrowableSet must be used. To let ExceptionManager to know the region crossing time, ie qApp->processEvents() call, we use the convention that the call to saveThrowableSet() is done 'just before' the qApp->processEvents() call. Where 'just before' it means that no others ExceptionManager method must be called between saveThrowableSet() and processEvents(). int currentRegionId = saveThrowableSet(); .....(no more ExceptionManager calls)..... qApp->processEvents(); .....(no ExceptionManager calls)..... restoreThrowableSet(currentRegionId); The region throwable sets are saved in a list: throwableSetList When a call to raise(excp) occurs totalThrowableSet is walked to find excp. If the exception is found then the exception is tagged to be thrown. In this case the flag isRaised is set in _all_ the occurrences of excp in the regionThrowableSet and in _all_ the occurrences of excp in throwableSetList. This is because the exception will be thrown in each region upon re-entering, not only in the current region. And in the same region will be thrown as many times as are the occurrences of excp in the corresponding throwable set Upon restoring the throwable set with restoreThrowableSet() it is safe to throw any pending exception with: throwPending(); Method throwPending() walks _in order_ the regionThrowableSet ONLY to find all the exceptions with the flag isRaised set. This is because C++ throw-catch does not seem to be able to walk-back across qApp->processEvents() boundaries, ie across regions. So _only_ the pending exceptions of the current region will be thrown. The others will be eventually thrown later. ExceptionManager throws ONLY ONE exception among the matching exceptions set. Then, in the catch clause throwPending() is called again. This is to guarantee that exceptions are thrown in the correct order. Note that the catch clause is always in the same region of the throw command. The exception thrown is the last that has been added -Removing exceptions from throwables list Normally an exception is removed from throwables list by code when leaving try block. But thrown exceptions will by-pass try block and will go directly in the catch clause. So all the exceptions added in try block must be removed in the catch clause, also if the thrown exception is not handled there. Note that in the catch clause all the exceptions of the catch set will be removed. If there are two exceptions raised, the first will throw and the catch clause will remove both. So the second exception will never be thrown. This is to take in account when adding exceptions in the try clause: NO!!! YES try { try { EM_REGISTER(very_bad_one); EM_REGISTER(small_one); EM_REGISTER(small_one); EM_REGISTER(very_bad_one); When a remove(excp) occurs regionThrowableSet and totalThrowableSet are walked in order from newest entry to oldest and the first occurrence of excp is removed. So to take in account the case where the same exception is added twice: fa() { ...... try { EM_REGISTER(myExcp); .......(no processEvents() here, same region) fb(); .......(no processEvents() here, same region) EM_REMOVE(myExcp); } catch () Where fb() { ...... try { EM_REGISTER(myExcp); ....... EM_REMOVE(myExcp); } catch () In this case myExcp will be added twice and must be removed twice. And after the first remove in fb() catch clause will be thrown again and caught in the fa() catch clause. So at the end of the catch clause there must always be the throwPending() call. qgit-2.7/qbs-build.sh000077500000000000000000000002621305655150700145560ustar00rootroot00000000000000#!/bin/bash qbs build \ --file qgit.qbs \ --build-directory ./build \ --command-echo-mode command-line \ --jobs 4 \ --no-install \ profile:qt486 release qgit-2.7/qbs/000077500000000000000000000000001305655150700131225ustar00rootroot00000000000000qgit-2.7/qbs/imports/000077500000000000000000000000001305655150700146175ustar00rootroot00000000000000qgit-2.7/qbs/imports/QbsFunctions/000077500000000000000000000000001305655150700172355ustar00rootroot00000000000000qgit-2.7/qbs/imports/QbsFunctions/functions.js000066400000000000000000000011551305655150700216050ustar00rootroot00000000000000 function versionIsAtLeast(actualVersion, expectedVersion) { var actualVersionParts = actualVersion.split('.').map(function(item) { return parseInt(item, 10); }); var expectedVersionParts = expectedVersion.split('.').map(function(item) { return parseInt(item, 10); }); for (var i = 0; i < expectedVersionParts.length; ++i) { if (actualVersionParts[i] > expectedVersionParts[i]) return true; if (actualVersionParts[i] < expectedVersionParts[i]) return false; } return i === expectedVersionParts.length || expectedVersionParts[i] === 0; } qgit-2.7/qgit.appdata.xml000066400000000000000000000014321305655150700154340ustar00rootroot00000000000000 qgit.desktop CC0-1.0 GPL-2.0 QGit A git GUI viewer

QGit is a graphical viewer for git repositories. Main features:

  • Graphical view of history, commits and the differences for the changed files
  • Quick access to git operations
http://libre.tibirna.org/projects/qgit http://libre.tibirna.org/projects/qgit/issues qgit
qgit-2.7/qgit.desktop000077500000000000000000000002421305655150700146750ustar00rootroot00000000000000[Desktop Entry] Type=Application Name=qgit GenericName=QGit Comment=A git GUI viewer Exec=qgit Icon=qgit Categories=Development;RevisionControl;Qt Terminal=false qgit-2.7/qgit.pro000066400000000000000000000000711305655150700140210ustar00rootroot00000000000000TEMPLATE=subdirs SUBDIRS=src CONFIG += debug_and_release qgit-2.7/qgit.qbs000066400000000000000000000003611305655150700140100ustar00rootroot00000000000000import qbs Project { name: "QGit" minimumQbsVersion: "1.5.0" property string minimumQtVersion: "4.8.2" property bool conversionWarnEnabled: true qbsSearchPaths: ["qbs"] references: [ "src/src.qbs", ] } qgit-2.7/qgit.sln000066400000000000000000000015271305655150700140240ustar00rootroot00000000000000 Microsoft Visual Studio Solution File, Format Version 10.00 # Visual C++ Express 2008 Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "qgit", "qgit.vcproj", "{40FC69D6-9066-41F6-8D9E-63886F5F058E}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Win32 = Debug|Win32 Release|Win32 = Release|Win32 EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution {40FC69D6-9066-41F6-8D9E-63886F5F058E}.Debug|Win32.ActiveCfg = Debug|Win32 {40FC69D6-9066-41F6-8D9E-63886F5F058E}.Debug|Win32.Build.0 = Debug|Win32 {40FC69D6-9066-41F6-8D9E-63886F5F058E}.Release|Win32.ActiveCfg = Release|Win32 {40FC69D6-9066-41F6-8D9E-63886F5F058E}.Release|Win32.Build.0 = Release|Win32 EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE EndGlobalSection EndGlobal qgit-2.7/qgit.vcproj000066400000000000000000000243471305655150700145400ustar00rootroot00000000000000 qgit-2.7/qgit_inno_setup.iss000066400000000000000000000070001305655150700162610ustar00rootroot00000000000000; QGit installation script for Inno Setup compiler ; ; QGit should be compiled with MSVC 2008, statically linked ; to Qt4.3 or better Trolltech libraries and directory of ; Visual C++ redistributable dll files copied under 'bin\' ; directory [Setup] AppName=QGit AppVerName=QGit version 2.3 DefaultDirName={pf}\QGit DefaultGroupName=QGit UninstallDisplayIcon={app}\qgit.exe Compression=lzma SolidCompression=yes LicenseFile=COPYING.rtf SetupIconFile=src\resources\qgit.ico OutputDir=bin OutputBaseFilename=qgit-2.3_win [Files] Source: "bin\qgit.exe"; DestDir: "{app}" Source: "bin\qgit.exe.manifest"; DestDir: "{app}" Source: "README_WIN.txt"; DestDir: "{app}"; Flags: isreadme ; Directory of MSVC redistributable files must be copied under 'bin\' ; before to run Inno Setup compiler or following line will error out Source: "bin\Microsoft.VC90.CRT\*"; DestDir: "{app}\Microsoft.VC90.CRT" [Tasks] Name: desktopicon; Description: "Create a &desktop icon"; Name: winexplorer; Description: "Add ""QGit Here"" in Windows Explorer context menu"; [Registry] Root: HKCU; Subkey: "Software\qgit"; Flags: uninsdeletekey Root: HKCU; Subkey: "Software\qgit\qgit4"; ValueType: string; ValueName: "msysgit_exec_dir"; ValueData: "{code:GetMSysGitExecDir}"; ; Windows Explorer integration Root: HKCU; Subkey: "SOFTWARE\Classes\Directory\shell\qgit"; Flags: uninsdeletekey; Tasks: winexplorer Root: HKCU; Subkey: "SOFTWARE\Classes\Directory\shell\qgit"; ValueType: string; ValueName: ""; ValueData: "&QGit Here"; Tasks: winexplorer Root: HKCU; Subkey: "SOFTWARE\Classes\Directory\shell\qgit\command"; ValueType: string; ValueName: ""; ValueData: "{app}\qgit.exe"; Tasks: winexplorer [Dirs] Name: {code:GetMSysGitExecDir}; Flags: uninsneveruninstall [Icons] Name: "{group}\QGit"; Filename: "{app}\qgit.exe"; WorkingDir: "%USERPROFILE%"; Name: "{group}\Uninstall QGit"; Filename: "{uninstallexe}" Name: "{commondesktop}\QGit"; Filename: "{app}\qgit.exe"; WorkingDir: "%USERPROFILE%"; Tasks: desktopicon [Code] var MSysGitDirPage: TInputDirWizardPage; procedure InitializeWizard; var Key, Val: String; begin // Create msysgit directory find page MSysGitDirPage := CreateInputDirPage(wpSelectProgramGroup, 'Select MSYSGIT Location', 'Where is MSYSGIT directory located?', 'Select where MSYSGIT directory is located, then click Next.', False, ''); // Add item (with an empty caption) MSysGitDirPage.Add(''); // Set initial value Key := 'SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\Git_is1'; if RegQueryStringValue(HKEY_LOCAL_MACHINE, Key, 'InstallLocation', Val) then begin MSysGitDirPage.Values[0] := Val; end else MSysGitDirPage.Values[0] := ExpandConstant('{pf}\Git'); end; function NextButtonClick(CurPageID: Integer): Boolean; var BaseDir: String; begin // Validate pages before allowing the user to proceed } if CurPageID = MSysGitDirPage.ID then begin BaseDir := MSysGitDirPage.Values[0]; if FileExists(ExpandFileName(BaseDir + '\bin\git.exe')) then begin Result := True; end else if FileExists(ExpandFileName(BaseDir + '\..\bin\git.exe')) then begin // sub dir selected MSysGitDirPage.Values[0] := ExpandFileName(BaseDir + '\..'); Result := True; end else begin MsgBox('Directory ''' + BaseDir + ''' does not seem the msysgit one, retry', mbError, MB_OK); Result := False; end; end else Result := True; end; function GetMSysGitExecDir(Param: String): String; begin Result := MSysGitDirPage.Values[0] + '\bin'; // already validated end; qgit-2.7/src/000077500000000000000000000000001305655150700131245ustar00rootroot00000000000000qgit-2.7/src/.gitignore000066400000000000000000000000201305655150700151040ustar00rootroot00000000000000Makefile* .* *~ qgit-2.7/src/FileHistory.cc000066400000000000000000000155071305655150700157040ustar00rootroot00000000000000/* Description: interface to git programs Author: Marco Costalba (C) 2005-2007 Copyright: See COPYING file that comes with this distribution */ #include "FileHistory.h" #include #include #include #include "lanes.h" #include "git.h" using namespace QGit; FileHistory::FileHistory(QObject* p, Git* g) : QAbstractItemModel(p), git(g) { headerInfo << "Graph" << "Id" << "Short Log" << "Author" << "Author Date"; lns = new Lanes(); revs.reserve(QGit::MAX_DICT_SIZE); clear(); // after _headerInfo is set connect(git, SIGNAL(newRevsAdded(const FileHistory*, const QVector&)), this, SLOT(on_newRevsAdded(const FileHistory*, const QVector&))); connect(git, SIGNAL(loadCompleted(const FileHistory*, const QString&)), this, SLOT(on_loadCompleted(const FileHistory*, const QString&))); connect(git, SIGNAL(changeFont(const QFont&)), this, SLOT(on_changeFont(const QFont&))); } FileHistory::~FileHistory() { clear(); delete lns; } void FileHistory::resetFileNames(SCRef fn) { fNames.clear(); fNames.append(fn); curFNames = fNames; } int FileHistory::rowCount(const QModelIndex& parent) const { return (!parent.isValid() ? rowCnt : 0); } bool FileHistory::hasChildren(const QModelIndex& parent) const { return !parent.isValid(); } int FileHistory::row(SCRef sha) const { const Rev* r = git->revLookup(sha, this); return (r ? r->orderIdx : -1); } const QString FileHistory::sha(int row) const { return (row < 0 || row >= rowCnt ? "" : QString(revOrder.at(row))); } void FileHistory::flushTail() { if (earlyOutputCnt < 0 || earlyOutputCnt >= revOrder.count()) { dbp("ASSERT in FileHistory::flushTail(), earlyOutputCnt is %1", earlyOutputCnt); return; } int cnt = revOrder.count() - earlyOutputCnt + 1; beginResetModel(); while (cnt > 0) { const ShaString& sha = revOrder.last(); const Rev* c = revs[sha]; delete c; revs.remove(sha); revOrder.pop_back(); cnt--; } // reset all lanes, will be redrawn for (int i = earlyOutputCntBase; i < revOrder.count(); i++) { Rev* c = const_cast(revs[revOrder[i]]); c->lanes.clear(); } firstFreeLane = earlyOutputCntBase; lns->clear(); rowCnt = revOrder.count(); endResetModel(); } void FileHistory::clear(bool complete) { if (!complete) { if (revOrder.count() > 0) flushTail(); return; } git->cancelDataLoading(this); beginResetModel(); qDeleteAll(revs); revs.clear(); revOrder.clear(); firstFreeLane = loadTime = earlyOutputCntBase = 0; setEarlyOutputState(false); lns->clear(); fNames.clear(); curFNames.clear(); qDeleteAll(rowData); rowData.clear(); if (testFlag(REL_DATE_F)) { secs = QDateTime::currentDateTime().toTime_t(); headerInfo[4] = "Last Change"; } else { secs = 0; headerInfo[4] = "Author Date"; } rowCnt = revOrder.count(); annIdValid = false; endResetModel(); emit headerDataChanged(Qt::Horizontal, 0, 4); } void FileHistory::on_newRevsAdded(const FileHistory* fh, const QVector& shaVec) { if (fh != this) // signal newRevsAdded() is broadcast return; // do not process revisions if there are possible renamed points // or pending renamed patch to apply if (!renamedRevs.isEmpty() || !renamedPatches.isEmpty()) return; // do not attempt to insert 0 rows since the inclusive range would be invalid if (rowCnt == shaVec.count()) return; beginInsertRows(QModelIndex(), rowCnt, shaVec.count()-1); rowCnt = shaVec.count(); endInsertRows(); } void FileHistory::on_loadCompleted(const FileHistory* fh, const QString&) { if (fh != this || rowCnt >= revOrder.count()) return; // now we can process last revision rowCnt = revOrder.count(); beginResetModel(); // force a reset to avoid artifacts in file history graph under Windows endResetModel(); // adjust Id column width according to the numbers of revisions we have if (!git->isMainHistory(this)) on_changeFont(QGit::STD_FONT); } void FileHistory::on_changeFont(const QFont& f) { QString maxStr(QString::number(rowCnt).length() + 1, '8'); QFontMetrics fmRows(f); int neededWidth = fmRows.boundingRect(maxStr).width(); QString id("Id"); QFontMetrics fmId(qApp->font()); while (fmId.boundingRect(id).width() < neededWidth) id += ' '; headerInfo[1] = id; emit headerDataChanged(Qt::Horizontal, 1, 1); } Qt::ItemFlags FileHistory::flags(const QModelIndex&) const { return Qt::ItemIsEnabled | Qt::ItemIsSelectable; // read only } QVariant FileHistory::headerData(int section, Qt::Orientation orientation, int role) const { if (orientation == Qt::Horizontal && role == Qt::DisplayRole) return headerInfo.at(section); return QVariant(); } QModelIndex FileHistory::index(int row, int column, const QModelIndex&) const { /* index() is called much more then data(), also by a 100X factor on big archives, so we use just the row number as QModelIndex payload and defer the revision lookup later, inside data(). Because row and column info are stored anyway in QModelIndex we don't need to add any additional data. */ if (row < 0 || row >= rowCnt) return QModelIndex(); return createIndex(row, column, (void*)0); } QModelIndex FileHistory::parent(const QModelIndex&) const { static const QModelIndex no_parent; return no_parent; } const QString FileHistory::timeDiff(unsigned long secs) const { ulong days = secs / (3600 * 24); ulong hours = (secs - days * 3600 * 24) / 3600; ulong min = (secs - days * 3600 * 24 - hours * 3600) / 60; ulong sec = secs - days * 3600 * 24 - hours * 3600 - min * 60; QString tmp; if (days > 0) tmp.append(QString::number(days) + "d "); if (hours > 0 || !tmp.isEmpty()) tmp.append(QString::number(hours) + "h "); if (min > 0 || !tmp.isEmpty()) tmp.append(QString::number(min) + "m "); tmp.append(QString::number(sec) + "s"); return tmp; } QVariant FileHistory::data(const QModelIndex& index, int role) const { static const QVariant no_value; if (!index.isValid() || role != Qt::DisplayRole) return no_value; // fast path, 90% of calls ends here! const Rev* r = git->revLookup(revOrder.at(index.row()), this); if (!r) return no_value; int col = index.column(); // calculate lanes if (r->lanes.count() == 0) git->setLane(r->sha(), const_cast(this)); if (col == QGit::ANN_ID_COL) return (annIdValid ? rowCnt - index.row() : QVariant()); if (col == QGit::LOG_COL) return r->shortLog(); if (col == QGit::AUTH_COL) return r->author(); if (col == QGit::TIME_COL && r->sha() != QGit::ZERO_SHA_RAW) { if (secs != 0) // secs is 0 for absolute date return timeDiff(secs - r->authorDate().toULong()); else return git->getLocalDate(r->authorDate()); } return no_value; } qgit-2.7/src/FileHistory.h000066400000000000000000000041411305655150700155360ustar00rootroot00000000000000/* Description: interface to git programs Author: Marco Costalba (C) 2005-2007 Copyright: See COPYING file that comes with this distribution */ #ifndef FILEHISTORY_H #define FILEHISTORY_H #include #include "common.h" //class Annotate; //class DataLoader; class Git; class Lanes; class FileHistory : public QAbstractItemModel { Q_OBJECT public: FileHistory(QObject* parent, Git* git); ~FileHistory(); void clear(bool complete = true); const QString sha(int row) const; int row(SCRef sha) const; const QStringList fileNames() const { return fNames; } void resetFileNames(SCRef fn); void setEarlyOutputState(bool b = true) { earlyOutputCnt = (b ? earlyOutputCntBase : -1); } void setAnnIdValid(bool b = true) { annIdValid = b; } virtual QVariant data(const QModelIndex &index, int role) const; virtual Qt::ItemFlags flags(const QModelIndex& index) const; virtual QVariant headerData(int s, Qt::Orientation o, int role = Qt::DisplayRole) const; virtual QModelIndex index(int r, int c, const QModelIndex& par = QModelIndex()) const; virtual QModelIndex parent(const QModelIndex& index) const; virtual int rowCount(const QModelIndex& par = QModelIndex()) const; virtual bool hasChildren(const QModelIndex& par = QModelIndex()) const; virtual int columnCount(const QModelIndex&) const { return 5; } public slots: void on_changeFont(const QFont&); private slots: void on_newRevsAdded(const FileHistory*, const QVector&); void on_loadCompleted(const FileHistory*, const QString&); private: friend class Annotate; friend class DataLoader; friend class Git; void flushTail(); const QString timeDiff(unsigned long secs) const; Git* git; RevMap revs; ShaVect revOrder; Lanes* lns; uint firstFreeLane; QList rowData; QList headerInfo; int rowCnt; bool annIdValid; unsigned long secs; int loadTime; int earlyOutputCnt; int earlyOutputCntBase; QStringList fNames; QStringList curFNames; QStringList renamedRevs; QHash renamedPatches; }; #endif // FILEHISTORY_H qgit-2.7/src/annotate.cpp000066400000000000000000000601301305655150700154410ustar00rootroot00000000000000/* Description: file history annotation Author: Marco Costalba (C) 2005-2007 Copyright: See COPYING file that comes with this distribution */ #include #include #include "FileHistory.h" #include "git.h" #include "myprocess.h" #include "annotate.h" #define MAX_AUTHOR_LEN 16 using namespace QGit; Annotate::Annotate(Git* parent, QObject* guiObj) : QObject(parent) { EM_INIT(exAnnCanceled, "Canceling annotation"); git = parent; gui = guiObj; cancelingAnnotate = annotateRunning = annotateActivity = false; valid = canceled = isError = false; connect(this, SIGNAL(annotateReady(Annotate*, bool, const QString&)), git, SIGNAL(annotateReady(Annotate*, bool, const QString&))); } const FileAnnotation* Annotate::lookupAnnotation(SCRef sha) { if (!valid || sha.isEmpty()) return NULL; AnnotateHistory::const_iterator it = ah.constFind(toTempSha(sha)); if (it != ah.constEnd()) return &(it.value()); // ok, we are not lucky. Check for an ancestor before to give up int shaIdx; const QString ancestorSha = getAncestor(sha, &shaIdx); if (!ancestorSha.isEmpty()) { it = ah.constFind(toTempSha(ancestorSha)); if (it != ah.constEnd()) return &(it.value()); } return NULL; } void Annotate::deleteWhenDone() { if (!EM_IS_PENDING(exAnnCanceled)) EM_RAISE(exAnnCanceled); if (annotateRunning) cancelingAnnotate = true; on_deleteWhenDone(); } void Annotate::on_deleteWhenDone() { if (!(annotateRunning || EM_IS_PENDING(exAnnCanceled))) deleteLater(); else QTimer::singleShot(20, this, SLOT(on_deleteWhenDone())); } bool Annotate::start(const FileHistory* _fh) { // could change during annotation, so save them fh = _fh; histRevOrder = fh->revOrder; if (histRevOrder.isEmpty()) { valid = false; return false; } annotateRunning = true; // init AnnotateHistory annFilesNum = 0; annId = histRevOrder.count(); annNumLen = QString::number(histRevOrder.count()).length(); ShaVect::const_iterator it(histRevOrder.constBegin()); do ah.insert(*it, FileAnnotation(annId--)); while (++it != histRevOrder.constEnd()); // annotating the file history could be time consuming, // so return now and use a timer to start annotation QTimer::singleShot(100, this, SLOT(slotComputeDiffs())); return true; } void Annotate::slotComputeDiffs() { processingTime.start(); if (!cancelingAnnotate) annotateFileHistory(); // now could call Qt event loop valid = !(isError || cancelingAnnotate); canceled = cancelingAnnotate; cancelingAnnotate = annotateRunning = false; if (canceled) deleteWhenDone(); else { QString msg("%1 %2"); msg = msg.arg(ah.count()).arg(processingTime.elapsed()); emit annotateReady(this, valid, msg); } } void Annotate::annotateFileHistory() { // sweep from the oldest to newest so that parent // annotations are calculated before children ShaVect::const_iterator it(histRevOrder.constEnd()); do { --it; doAnnotate(*it); } while (it != histRevOrder.constBegin() && !isError && !cancelingAnnotate); } void Annotate::doAnnotate(const ShaString& ss) { // all the parents annotations must be valid here const QString sha(ss); FileAnnotation* fa = getFileAnnotation(sha); if (fa == NULL || fa->isValid || isError || cancelingAnnotate) return; const Rev* r = git->revLookup(ss, fh); // historyRevs if (r == NULL) { dbp("ASSERT doAnnotate: no revision %1", sha); isError = true; return; } const QString& diff(getPatch(sha)); // set FileAnnotation::fileSha if (r->parentsCount() == 0) { // initial revision setInitialAnnotation(ah[ss].fileSha, fa); // calls Qt event loop fa->isValid = true; return; } // now create a new annotation from first parent diffs const QStringList& parents(r->parents()); const QString& parSha = parents.first(); FileAnnotation* pa = getFileAnnotation(parSha); if (!pa || !pa->isValid) { dbp("ASSERT in doAnnotate: annotation for %1 not valid", parSha); isError = true; return; } const QString& author(setupAuthor(r->author(), fa->annId)); setAnnotation(diff, author, pa->lines, fa->lines); // then add other parents diff if any QStringList::const_iterator it(parents.constBegin()); ++it; int parentNum = 1; while (it != parents.constEnd()) { FileAnnotation* pa = getFileAnnotation(*it); const QString& diff(getPatch(sha, parentNum++)); QStringList tmpAnn; setAnnotation(diff, "Merge", pa->lines, tmpAnn); // the two annotations must be of the same length if (fa->lines.count() != tmpAnn.count()) { qDebug("ASSERT: merging annotations of different length\n merging " "%s in %s", (*it).toLatin1().constData(), sha.toLatin1().constData()); isError = true; return; } // finally we unify the annotations unify(tmpAnn, fa->lines); fa->lines = tmpAnn; ++it; } fa->isValid = true; } FileAnnotation* Annotate::getFileAnnotation(SCRef sha) { AnnotateHistory::iterator it(ah.find(toTempSha(sha))); if (it == ah.end()) { dbp("ASSERT getFileAnnotation: no revision %1", sha); isError = true; return NULL; } return &(*it); } void Annotate::setInitialAnnotation(SCRef fileSha, FileAnnotation* fa) { QByteArray fileData; // fh->fileNames() are in cronological order, so we need the last one git->getFile(fileSha, NULL, &fileData, fh->fileNames().last()); // calls Qt event loop if (cancelingAnnotate) return; int lineNum = fileData.count('\n'); if (!fileData.endsWith('\n') && !fileData.isEmpty()) // No newline at end of file lineNum++; const QString empty; for (int i = 0; i < lineNum; i++) fa->lines.append(empty); } const QString Annotate::setupAuthor(SCRef origAuthor, int annId) { QString tmp(origAuthor.section('<', 0, 0).trimmed()); // strip e-mail address if (tmp.isEmpty()) { // probably only e-mail tmp = origAuthor; tmp.remove('<').remove('>'); tmp = tmp.trimmed(); tmp.truncate(MAX_AUTHOR_LEN); } // shrink author name if necessary if (tmp.length() > MAX_AUTHOR_LEN) { SCRef firstName(tmp.section(' ', 0, 0).trimmed()); SCRef surname(tmp.section(' ', 1).trimmed()); if (!firstName.isEmpty() && !surname.isEmpty()) tmp = firstName.left(1) + ". " + surname; tmp.truncate(MAX_AUTHOR_LEN); } return QString("%1.%2").arg(annId, annNumLen).arg(tmp); } void Annotate::unify(SList dst, SCList src) { const QString m("Merge"); for (int i = 0; i < dst.size(); ++i) { if (dst.at(i) == m) dst[i] = src.at(i); } } bool Annotate::setAnnotation(SCRef diff, SCRef author, SCList prevAnn, SList newAnn, int ofs) { newAnn.clear(); QStringList::const_iterator cur(prevAnn.constBegin()); QString line; int idx = 0, num, lineNumStart, lineNumEnd; int curLineNum = 1; // warning, starts from 1 instead of 0 bool inHeader = true; while (getNextSection(diff, idx, line, "\n")) { char firstChar = line.at(0).toLatin1(); if (inHeader) { if (firstChar == '@') inHeader = false; else continue; } switch (firstChar) { case '@': // an unified diff fragment header has form '@@ -a,b +c,d @@' // where 'a' is old file line number and 'b' is old file // number of lines of the hunk, 'c' and 'd' are the same // for new file. If the file does not have enough lines // then also the form '@@ -a +c @@' is used. if (ofs == 0) lineNumStart = line.indexOf('-') + 1; else // in this case we are given diff fragments with // faked small files that span the fragment plus // some padding. So we use 'c' instead of 'a' to // find the beginning of our patch in the faked file, // this value will be offsetted by ofs later lineNumStart = line.indexOf('+') + 1; lineNumEnd = line.indexOf(',', lineNumStart); if (lineNumEnd == -1) // small file case lineNumEnd = line.indexOf(' ', lineNumStart); num = line.mid(lineNumStart, lineNumEnd - lineNumStart).toInt(); num -= ofs; // offset for range filter computation // diff lines start from 1, 0 is empty file, // instead QValueList::at() starts from 0 if (num < 0 || num > prevAnn.size()) { dbp("ASSERT setAnnotation: start line number is %1", num); isError = true; return false; } for ( ; curLineNum < num; ++curLineNum) { newAnn.append(*cur); ++cur; } break; case '+': newAnn.append(author); break; case '-': if (curLineNum > prevAnn.size()) { dbp("ASSERT setAnnotation: remove end of " "file, diff is %1", diff); isError = true; return false; } else { ++cur; ++curLineNum; } break; case '\\': // diff(1) produces a "\ No newline at end of file", but the // message is locale dependent, so just test the space after '\' if (line[1] == ' ') break; // fall through default: if (curLineNum > prevAnn.size()) { dbp("ASSERT setAnnotation: end of " "file reached, diff is %1", diff); isError = true; return false; } else { newAnn.append(*cur); ++cur; ++curLineNum; } break; } } // copy the tail for ( ; curLineNum <= prevAnn.size(); ++curLineNum) { newAnn.append(*cur); ++cur; } return true; } const QString Annotate::getPatch(SCRef sha, int parentNum) { QString mergeSha(sha); if (parentNum) mergeSha = QString::number(parentNum) + " m " + sha; const Rev* r = git->revLookup(mergeSha, fh); if (!r) return QString(); const QString diff(r->diff()); QVector ba; const ShaString& ss = toPersistentSha(sha, ba); if (ah[ss].fileSha.isEmpty() && !parentNum) { int idx = diff.indexOf(".."); if (idx != -1) ah[ss].fileSha = diff.mid(idx + 2, 40); else // file mode change only, same sha of parent ah[ss].fileSha = ah[r->parent(0)].fileSha; } return diff; } bool Annotate::getNextSection(SCRef d, int& idx, QString& sec, SCRef target) { if (idx >= d.length()) return false; int newIdx = d.indexOf(target, idx); if (newIdx == -1) // last section, take all newIdx = d.length() - 1; sec = d.mid(idx, newIdx - idx + 1); idx = newIdx + 1; return true; } // ****************************** RANGE FILTER ***************************** bool Annotate::getRange(SCRef sha, RangeInfo* r) { if (!ranges.contains(sha) || !valid || canceled) { r->clear(); return false; } *r = ranges[sha]; // by copy return true; } void Annotate::updateCrossRanges(SCRef chunk, bool rev, int fileLen, int ofs, RangeInfo* r) { /* here the deal is to fake a file that will be modified by chunk, the file must contain also the whole output range. Then we apply an annotation step and see what happens... First we mark each line of the file with the corresponding line number, then apply the patch and check where the lines have been moved around. Now we have to cases: - In reverse case we infer the before-patch range knowing the after-patch range. So we check what lines we have in the region corresponding to after-patch range. - In forward case we infer the after-patch range knowing the before-patch range. So we scan the resulting annotation to find line numbers corresponding to the before-patch range. */ // because of the padding the file first line number will be // // fileFirstLineNr = newLineId - beforePadding = fileOffset + 1 QStringList beforeAnn; QStringList afterAnn; for (int lineNum = ofs + 1; lineNum <= ofs + fileLen; lineNum++) beforeAnn.append(QString::number(lineNum)); const QString fakedAuthor("*"); setAnnotation(chunk, fakedAuthor, beforeAnn, afterAnn, ofs); int newStart = ofs + 1; int newEnd = ofs + fileLen; if (rev) { // let's see what line number we have at given range interval limits. // at() counts from 0. //QLinkedList::const_iterator itStart(afterAnn.at(r->start - ofs - 1)); //QLinkedList::const_iterator itEnd(afterAnn.at(r->end - ofs - 1)); QStringList::const_iterator itStart(afterAnn.constBegin()); for (int i = 0; i < r->start - ofs - 1; i++) ++itStart; QStringList::const_iterator itEnd(afterAnn.constBegin()); for (int i = 0; i < r->end - ofs - 1; i++) ++itEnd; bool leftExtended = (*itStart == fakedAuthor); bool rightExtended = (*itEnd == fakedAuthor); // if range boundary is a line added by the patch // we consider inclusive and extend the range ++itStart; do { --itStart; if (*itStart != fakedAuthor) { newStart = (*itStart).toInt(); break; } } while (itStart != afterAnn.constBegin()); while (itEnd != afterAnn.constEnd()) { if (*itEnd != fakedAuthor) { newEnd = (*itEnd).toInt(); break; } ++itEnd; } if (leftExtended && *itStart != fakedAuthor) newStart++; if (rightExtended && itEnd != afterAnn.constEnd()) newEnd--; r->modified = (leftExtended || rightExtended); if (!r->modified) { // check for consecutive sequence for (int i = r->start; i <= r->end; ++i, ++itStart) if (i - r->start != (*itStart).toInt() - newStart) { r->modified = true; break; } } if (newStart > newEnd) // selected range is whole inside new added lines newStart = newEnd = 0; } else { // forward case // scan afterAnn to check for before-patch range boundaries QStringList::const_iterator itStart(afterAnn.constEnd()); QStringList::const_iterator itEnd(afterAnn.constEnd()); QStringList::const_iterator it(afterAnn.constBegin()); for (int lineNum = ofs + 1; it != afterAnn.constEnd(); ++lineNum, ++it) { if (*it != fakedAuthor) { if ((*it).toInt() <= r->start) { newStart = lineNum; itStart = it; } if ( (*it).toInt() >= r->end && itEnd == afterAnn.constEnd()) { // one-shot newEnd = lineNum; itEnd = it; } } } if (itStart != afterAnn.constEnd() && (*itStart).toInt() < r->start) newStart++; if (itEnd != afterAnn.constEnd() && (*itEnd).toInt() > r->end) newEnd--; r->modified = (itStart == afterAnn.constEnd() || itEnd == afterAnn.constEnd()); if (!r->modified) { // check for consecutive sequence for (int i = r->start; i <= r->end; ++itStart, i++) if ((*itStart).toInt() != i) { r->modified = true; break; } } if (newStart > newEnd) // selected range has been deleted newStart = newEnd = 0; } r->start = newStart; r->end = newEnd; } void Annotate::updateRange(RangeInfo* r, SCRef diff, bool reverse) { r->modified = false; if (r->start == 0) return; // r(start, end) is updated after each chunk incrementally and // not at the end of the whole diff to be always in sync with // chunk headers that are updated in the same way by GNU diff. // // so in case of reverse we have to apply the chunks from last // one to first. int idx = 0; QString chunk; QStringList chunkList; while (getNextSection(diff, idx, chunk, "\n@")) if (reverse) chunkList.prepend(chunk); else chunkList.append(chunk); QStringList::const_iterator chunkIt(chunkList.constBegin()); while (chunkIt != chunkList.constEnd()) { // an unified diff fragment header has form '@@ -a,b +c,d @@' // where 'a' is old file line number and 'b' is old file // number of lines of the hunk, 'c' and 'd' are the same // for new file. If the file does not have enough lines // then also the form '@@ -a +c @@' is used. chunk = *chunkIt++; int m = chunk.indexOf('-'); int c1 = chunk.indexOf(',', m); int p = chunk.indexOf('+', c1); int c2 = chunk.indexOf(',', p); int e = chunk.indexOf(' ', c2); int oldLineCnt = chunk.mid(c1 + 1, p - c1 - 2).toInt(); int newLineId = chunk.mid(p + 1, c2 - p - 1).toInt(); int newLineCnt = chunk.mid(c2 + 1, e - c2 - 1).toInt(); int lineNumDiff = newLineCnt - oldLineCnt; // because r(start, end) is updated after each chunk we have to // consider the updated patch delimiters to compare with r(start, end) int patchStart = newLineId; // patch end depends only to lines count so is... int patchEnd = patchStart + (reverse ? newLineCnt : oldLineCnt); patchEnd--; // with 1 line patch patchStart == patchEnd // case 1: patch range after our range if (patchStart > r->end) continue; // case 2: patch range before our range if (patchEnd < r->start) { r->start += (reverse ? -lineNumDiff : lineNumDiff); r->end += (reverse ? -lineNumDiff : lineNumDiff); continue; } // case 3: the patch is whole inside our range if (patchStart >= r->start && patchEnd <= r->end) { r->end += (reverse ? -lineNumDiff : lineNumDiff); r->modified = true; continue; } // case 4: ranges are crossing // add padding so that resulting file is the UNION: selectRange U patchRange // reverse independent int beforePadding = (r->start > patchStart ? 0 : patchStart - r->start); // reverse dependent int afterPadding = (patchEnd > r->end ? 0 : r->end - patchEnd); // file is the faked file on which we will apply the diff, // so it is always the _old_ before the patch one. int fileLenght = beforePadding + oldLineCnt + afterPadding; // given the chunk header @@ -a,b +c,d @@, line nr. 'c' must correspond // to the file line nr. 1 because we have only a partial file. // More, there is also the file padding to consider. // So we need that 'c' corresponds to file line nr. 'beforePadding + 1' // // the transformation in setAnnotation() is // newLineNum = 'c' - offset = beforePadding + 1 // so... int fileOffset = newLineId - beforePadding - 1; // because of the padding the file first line number will be // fileFirstLineNr = newLineId - beforePadding = fileOffset + 1 updateCrossRanges(chunk, reverse, fileLenght, fileOffset, r); } } const QString Annotate::getAncestor(SCRef sha, int* shaIdx) { QString fileSha; try { annotateActivity = true; EM_REGISTER(exAnnCanceled); QStringList fn(fh->fileNames()); FOREACH_SL (it, fn) { fileSha = git->getFileSha(*it, sha); // calls qApp->processEvents() if (!fileSha.isEmpty()) break; } if (fileSha.isEmpty()) { dbp("ASSERT in getAncestor: empty file from %1", sha); return ""; } EM_REMOVE(exAnnCanceled); annotateActivity = false; } catch(int i) { EM_REMOVE(exAnnCanceled); annotateActivity = false; if (EM_MATCH(i, exAnnCanceled, "getting ancestor")) { EM_THROW_PENDING; return ""; } const QString info("Exception \'" + EM_DESC(i) + "\' " "not handled in Annotation lookup...re-throw"); dbs(info); throw; } // NOTE: more then one revision could have the same file sha as our // input revision. This happens if the patch is reverted or if the patch // modify only file mode but no content. From the point of view of code // range filtering this is equivalent, so we don't care to find the correct // ancestor, but just the first revision with the same file sha for (*shaIdx = 0; *shaIdx < histRevOrder.count(); (*shaIdx)++) { const FileAnnotation& fa(ah[histRevOrder[*shaIdx]]); if (fa.fileSha == fileSha) return histRevOrder[*shaIdx]; } // ok still not found, this could happen if sha is an unapplied // stgit patch. In this case fall back on the first in the list // that is the newest. if (git->getAllRefSha(Git::UN_APPLIED).contains(sha)) return histRevOrder.first(); dbp("ASSERT in getAncestor: ancestor of %1 not found", sha); return ""; } bool Annotate::isDescendant(SCRef sha, SCRef target) { // quickly check if target is a direct descendant of sha, i.e. if starting // from target, sha could be reached walking along his parents. In case // a merge is found the search returns false because you'll need, // in general, all the previous ranges to compute the target one. const Rev* r = git->revLookup(sha, fh); if (!r) return false; int shaIdx = r->orderIdx; r = git->revLookup(target, fh); while (r && r->orderIdx < shaIdx && r->parentsCount() == 1) r = git->revLookup(r->parent(0), fh); return (r && r->orderIdx == shaIdx); } /* Range filtering uses only annotated added/removed lines to compute ranges, patch content is never checked, this algorithm is fast but fails in case of code shuffle; if a patch moves some code between two independents part of the same file this will be interpreted as a delete of origin code. New added code is not recognized as the same of old one because content is not checked. Only checking for copies would fix the corner case, but implementation is too difficult, so better accept this design limitation for now. */ const QString Annotate::computeRanges(SCRef sha, int paraFrom, int paraTo, SCRef target) { ranges.clear(); if (!valid || canceled || sha.isEmpty()) { dbp("ASSERT in computeRanges: annotation from %1 not valid", sha); return ""; } // paragraphs start from 0 but ranges from 1 int rangeStart = paraFrom + 1; int rangeEnd = paraTo + 1; QString ancestor(sha); int shaIdx; QVector ba; const ShaString& ss = toPersistentSha(sha, ba); for (shaIdx = 0; shaIdx < histRevOrder.count(); shaIdx++) if (histRevOrder[shaIdx] == ss) break; if (shaIdx == histRevOrder.count()) { // not in history, find an ancestor ancestor = getAncestor(sha, &shaIdx); if (ancestor.isEmpty()) return ""; } // insert starting one, always included by default, could be removed after ranges.insert(ancestor, RangeInfo(rangeStart, rangeEnd, true)); // check if target is a descendant, so to skip back history walking bool isDirectDescendant = isDescendant(ancestor, target); // going back in history, to oldest following first parent lane const QString oldest(histRevOrder.last()); // causes a detach! const Rev* curRev = git->revLookup(ancestor, fh); // historyRevs QString curRevSha(curRev->sha()); while (curRevSha != oldest && !isDirectDescendant) { const QString& diff(getPatch(curRevSha)); if (diff.isEmpty()) { if (curRev->parentsCount() == 0) // is initial break; dbp("ASSERT in rangeFilter 1: diff for %1 not found", curRevSha); return ""; } RangeInfo r(ranges[curRevSha]); updateRange(&r, diff, true); // special case for modified flag. Mark always the 'after patch' revision // with modified flag, not the before patch. So we have to stick the flag // to the newer revision. ranges[curRevSha].modified = r.modified; // if the second revision does not modify the range then r.modified == false // and the first revision range is created with modified == false, if the // first revision is initial then the loop is exited without update the flag // and the first revision is missed. // // we want to always include first revision as a compare base also if // does not modify anything. Of course range must be valid. r.modified = (r.start != 0); if (curRev->parentsCount() == 0) break; curRev = git->revLookup(curRev->parent(0), fh); curRevSha = curRev->sha(); ranges.insert(curRevSha, r); if (curRevSha == target) // stop now, no need to continue return ancestor; } // now that we have initial revision go back and sweep up all the // remaining stuff, we are guaranteed a good parent is always // found but in case of an independent branch, see below if (!isDirectDescendant) shaIdx = histRevOrder.count() - 1; for ( ; shaIdx >= 0; shaIdx--) { SCRef sha(histRevOrder[shaIdx]); if (!ranges.contains(sha)) { curRev = git->revLookup(sha, fh); if (curRev->parentsCount() == 0) { // the start of an independent branch is found in this case // insert an empty range, the whole branch will be ignored. // Merge of outside branches are very rare so this solution // seems enough if we don't want to dive in (useless) complications. ranges.insert(sha, RangeInfo()); continue; } const QString& diff(getPatch(sha)); if (diff.isEmpty()) { dbp("ASSERT in rangeFilter 2: diff for %1 not found", sha); return ""; } QString parSha(curRev->parent(0)); if (!ranges.contains(parSha)) { if (isDirectDescendant) // we must be in a parallel lane, no need continue; // to compute range info, simply go on dbp("ASSERT in rangeFilter: range info for %1 not found", parSha); return ""; } RangeInfo r(ranges[parSha]); updateRange(&r, diff, false); ranges.insert(sha, r); if (sha == target) // stop now, no need to continue return ancestor; } } return ancestor; } bool Annotate::seekPosition(int* paraFrom, int* paraTo, SCRef fromSha, SCRef toSha) { if ((*paraFrom == 0 && *paraTo == 0) || fromSha == toSha) return true; Ranges backup(ranges); // implicitly shared if (computeRanges(fromSha, *paraFrom, *paraTo, toSha).isEmpty()) goto fail; if (!ranges.contains(toSha)) goto fail; *paraFrom = ranges[toSha].start - 1; *paraTo = ranges[toSha].end - 1; ranges = backup; return true; fail: ranges = backup; return false; } qgit-2.7/src/annotate.h000066400000000000000000000044211305655150700151070ustar00rootroot00000000000000/* Author: Marco Costalba (C) 2005-2007 Copyright: See COPYING file that comes with this distribution */ #ifndef ANNOTATE_H #define ANNOTATE_H #include #include #include "exceptionmanager.h" #include "common.h" class Git; class FileHistory; class MyProcess; class RangeInfo { public: RangeInfo() { clear(); } RangeInfo(int s, int e, bool m) : start(s), end(e), modified(m) {} void clear() { start = end = 0; modified = false; } int start, end; // ranges count file lines from 1 like patches diffs bool modified; }; typedef QHash Ranges; class Annotate : public QObject { Q_OBJECT public: Annotate(Git* parent, QObject* guiObj); void deleteWhenDone(); const FileAnnotation* lookupAnnotation(SCRef sha); bool start(const FileHistory* fh); bool isCanceled() { return canceled; } const QString getAncestor(SCRef sha, int* shaIdx); bool getRange(SCRef sha, RangeInfo* r); bool seekPosition(int* rangeStart, int* rangeEnd, SCRef fromSha, SCRef toSha); const QString computeRanges(SCRef sha, int paraFrom, int paraTo, SCRef target = ""); signals: void annotateReady(Annotate*, bool, const QString&); private slots: void on_deleteWhenDone(); void slotComputeDiffs(); private: void annotateFileHistory(); void doAnnotate(const ShaString& sha); FileAnnotation* getFileAnnotation(SCRef sha); void setInitialAnnotation(SCRef fileSha, FileAnnotation* fa); const QString setupAuthor(SCRef origAuthor, int annId); bool setAnnotation(SCRef diff, SCRef aut, SCList pAnn, SList nAnn, int ofs = 0); bool getNextLine(SCRef d, int& idx, QString& line); static void unify(SList dst, SCList src); const QString getPatch(SCRef sha, int parentNum = 0); bool getNextSection(SCRef d, int& idx, QString& sec, SCRef target); void updateRange(RangeInfo* r, SCRef diff, bool reverse); void updateCrossRanges(SCRef cnk, bool rev, int oStart, int oLineCnt, RangeInfo* r); bool isDescendant(SCRef sha, SCRef target); EM_DECLARE(exAnnCanceled); Git* git; QObject* gui; const FileHistory* fh; AnnotateHistory ah; bool cancelingAnnotate; bool annotateRunning; bool annotateActivity; bool isError; int annNumLen; int annId; int annFilesNum; ShaVect histRevOrder; // TODO use reference bool valid; bool canceled; QTime processingTime; Ranges ranges; }; #endif qgit-2.7/src/app_icon.rc000066400000000000000000000000601305655150700152360ustar00rootroot00000000000000IDI_ICON1 ICON DISCARDABLE "resources\qgit.ico" qgit-2.7/src/cache.cpp000066400000000000000000000074241305655150700147020ustar00rootroot00000000000000/* Description: file names persistent cache Author: Marco Costalba (C) 2005-2007 Copyright: See COPYING file that comes with this distribution */ #include #include #include #include "cache.h" using namespace QGit; bool Cache::save(const QString& gitDir, const RevFileMap& rf, const StrVect& dirs, const StrVect& files) { if (gitDir.isEmpty() || rf.isEmpty()) return false; QString path(gitDir + C_DAT_FILE); QString tmpPath(path + BAK_EXT); QDir dir; if (!dir.exists(gitDir)) { dbs("Git directory not found, unable to save cache"); return false; } QFile f(tmpPath); if (!f.open(QIODevice::WriteOnly | QIODevice::Unbuffered)) return false; dbs("Saving cache. Please wait..."); // compress in memory before write to file QByteArray data; QDataStream stream(&data, QIODevice::WriteOnly); // Write a header with a "magic number" and a version stream << (quint32)C_MAGIC; stream << (qint32)C_VERSION; stream << (qint32)dirs.count(); for (int i = 0; i < dirs.count(); ++i) stream << dirs.at(i); stream << (qint32)files.count(); for (int i = 0; i < files.count(); ++i) stream << files.at(i); // to achieve a better compression we save the sha's as // one very long string instead of feeding the stream with // each one. With this trick we gain a 15% size reduction // in the final compressed file. The save/load speed is // almost the same. uint bufSize = rf.count() * 41 + 1000; // a little bit more space then required QByteArray buf; buf.reserve(bufSize); QVector v; v.reserve(rf.count()); QVector ba; ShaString CUSTOM_SHA_RAW(toPersistentSha(CUSTOM_SHA, ba)); unsigned int newSize = 0; FOREACH (RevFileMap, it, rf) { const ShaString& sha = it.key(); if ( sha == ZERO_SHA_RAW || sha == CUSTOM_SHA_RAW || sha.latin1()[0] == 'A') // ALL_MERGE_FILES + rev sha continue; v.append(it.value()); buf.append(sha.latin1()).append('\0'); newSize += 41; if (newSize > bufSize) { dbs("ASSERT in Cache::save, out of allocated space"); return false; } } buf.resize(newSize); stream << (qint32)newSize; stream << buf; for (int i = 0; i < v.size(); ++i) *(v.at(i)) >> stream; dbs("Compressing data..."); f.write(qCompress(data, 1)); // no need to encode with compressed data f.close(); // rename C_DAT_FILE + BAK_EXT -> C_DAT_FILE if (dir.exists(path)) { if (!dir.remove(path)) { dbs("access denied to " + path); dir.remove(tmpPath); return false; } } dir.rename(tmpPath, path); dbs("Done."); return true; } bool Cache::load(const QString& gitDir, RevFileMap& rfm, StrVect& dirs, StrVect& files, QByteArray& revsFilesShaBuf) { // check for cache file QString path(gitDir + C_DAT_FILE); QFile f(path); if (!f.exists()) return true; // no cache file is not an error if (!f.open(QIODevice::ReadOnly | QIODevice::Unbuffered)) return false; QDataStream stream(qUncompress(f.readAll())); quint32 magic; qint32 version; qint32 dirsNum, filesNum, bufSize; stream >> magic; stream >> version; if (magic != C_MAGIC || version != C_VERSION) { f.close(); return false; } // read the data stream >> dirsNum; dirs.resize(dirsNum); for (int i = 0; i < dirsNum; ++i) stream >> dirs[i]; stream >> filesNum; files.resize(filesNum); for (int i = 0; i < filesNum; ++i) stream >> files[i]; stream >> bufSize; revsFilesShaBuf.clear(); revsFilesShaBuf.reserve(bufSize); stream >> revsFilesShaBuf; const char* data = revsFilesShaBuf.constData(); while (!stream.atEnd()) { RevFile* rf = new RevFile(); *rf << stream; ShaString sha(data); rfm.insert(sha, rf); data += 40; if (*data != '\0') { dbp("ASSERT in Cache::load, corrupted SHA after %1", sha); return false; } data++; } f.close(); return true; } qgit-2.7/src/cache.h000066400000000000000000000010201305655150700143310ustar00rootroot00000000000000/* Author: Marco Costalba (C) 2005-2007 Copyright: See COPYING file that comes with this distribution */ #ifndef CACHE_H #define CACHE_H #include "git.h" class Cache : public QObject { Q_OBJECT public: explicit Cache(QObject* par) : QObject(par) {} static bool save(const QString& gitDir, const RevFileMap& rf, const StrVect& dirs, const StrVect& files); static bool load(const QString& gitDir, RevFileMap& rf, StrVect& dirs, StrVect& files, QByteArray& revsFilesShaBuf); }; #endif qgit-2.7/src/commit.ui000066400000000000000000000223641305655150700147620ustar00rootroot00000000000000 CommitBase 0 0 647 531 Commit changes :/icons/resources/vcs_commit.png 0 0 0 0 0 6 0 0 0 0 Qt::Vertical Qt::CustomContextMenu Check files to commit false false true 2 File Index status 0 0 0 0 0 Commit message (first line is the sub&ject): false textEditMsg Lines starting with '#' will be ignored Press <Ctrl+Enter> to commit changes QTextEdit::NoWrap false false 6 0 0 0 0 150 24 Monospace QFrame::Sunken Line: %1 Col: %2 false Qt::Horizontal QSizePolicy::Expanding 30 20 0 0 Settin&gs Alt+G false false 0 0 Canc&el Alt+E false true 0 0 Update index for selected files Update &Index 0 0 Commit selected files &Commit Alt+C pushButtonCancel pushButtonUpdateCache pushButtonOk pushButtonCancel clicked() CommitBase pushButtonCancel_clicked() 20 20 20 20 pushButtonUpdateCache clicked() CommitBase pushButtonUpdateCache_clicked() 20 20 20 20 pushButtonSettings clicked() CommitBase pushButtonSettings_clicked() 20 20 20 20 qgit-2.7/src/commitimpl.cpp000066400000000000000000000270471305655150700160140ustar00rootroot00000000000000/* Description: changes commit dialog Author: Marco Costalba (C) 2005-2007 Copyright: See COPYING file that comes with this distribution */ #include #include #include #include #include #include #include #include #include #include #include "exceptionmanager.h" #include "common.h" #include "git.h" #include "settingsimpl.h" #include "commitimpl.h" using namespace QGit; QString CommitImpl::lastMsgBeforeError; CommitImpl::CommitImpl(Git* g, bool amend) : git(g) { // adjust GUI setAttribute(Qt::WA_DeleteOnClose); setupUi(this); textEditMsg->setFont(TYPE_WRITER_FONT); QVector v(1, splitter); QGit::restoreGeometrySetting(CMT_GEOM_KEY, this, &v); QSettings settings; QString templ(settings.value(CMT_TEMPL_KEY, CMT_TEMPL_DEF).toString()); QString msg; QDir d; if (d.exists(templ)) readFromFile(templ, msg); // set-up files list const RevFile* f = git->getFiles(ZERO_SHA); for (int i = 0; f && i < f->count(); ++i) { // in case of amend f could be null bool inIndex = f->statusCmp(i, RevFile::IN_INDEX); bool isNew = (f->statusCmp(i, RevFile::NEW) || f->statusCmp(i, RevFile::UNKNOWN)); QColor myColor = QPalette().color(QPalette::WindowText); if (isNew) myColor = Qt::darkGreen; else if (f->statusCmp(i, RevFile::DELETED)) myColor = Qt::red; QTreeWidgetItem* item = new QTreeWidgetItem(treeWidgetFiles); item->setText(0, git->filePath(*f, i)); item->setText(1, inIndex ? "Updated in index" : "Not updated in index"); item->setCheckState(0, inIndex || !isNew ? Qt::Checked : Qt::Unchecked); item->setForeground(0, myColor); } treeWidgetFiles->resizeColumnToContents(0); // compute cursor offsets. Take advantage of fixed width font textEditMsg->setPlainText("\nx\nx"); // cursor doesn't move on empty text textEditMsg->moveCursor(QTextCursor::Start); textEditMsg->verticalScrollBar()->setValue(0); textEditMsg->horizontalScrollBar()->setValue(0); int y0 = textEditMsg->cursorRect().y(); int x0 = textEditMsg->cursorRect().x(); textEditMsg->moveCursor(QTextCursor::Down); textEditMsg->moveCursor(QTextCursor::Right); textEditMsg->verticalScrollBar()->setValue(0); int y1 = textEditMsg->cursorRect().y(); int x1 = textEditMsg->cursorRect().x(); ofsX = x1 - x0; ofsY = y1 - y0; textEditMsg->moveCursor(QTextCursor::Start); textEditMsg_cursorPositionChanged(); if (lastMsgBeforeError.isEmpty()) { // setup textEditMsg with old commit message to be amended QString status(""); if (amend) status = git->getLastCommitMsg(); // setup textEditMsg with default value if user opted to do so (default) if (testFlag(USE_CMT_MSG_F, FLAGS_KEY)) status += git->getNewCommitMsg(); msg = status.trimmed(); } else msg = lastMsgBeforeError; textEditMsg->setPlainText(msg); textEditMsg->setFocus(); // if message is not changed we avoid calling refresh // to change patch name in stgCommit() origMsg = msg; // setup button functions if (amend) { if (git->isStGITStack()) { pushButtonOk->setText("&Add to top"); pushButtonOk->setShortcut(QKeySequence("Alt+A")); pushButtonOk->setToolTip("Refresh top stack patch"); } else { pushButtonOk->setText("&Amend"); pushButtonOk->setShortcut(QKeySequence("Alt+A")); pushButtonOk->setToolTip("Amend latest commit"); } connect(pushButtonOk, SIGNAL(clicked()), this, SLOT(pushButtonAmend_clicked())); } else { if (git->isStGITStack()) { pushButtonOk->setText("&New patch"); pushButtonOk->setShortcut(QKeySequence("Alt+N")); pushButtonOk->setToolTip("Create a new patch"); } connect(pushButtonOk, SIGNAL(clicked()), this, SLOT(pushButtonCommit_clicked())); } connect(treeWidgetFiles, SIGNAL(customContextMenuRequested(const QPoint&)), this, SLOT(contextMenuPopup(const QPoint&))); connect(textEditMsg, SIGNAL(cursorPositionChanged()), this, SLOT(textEditMsg_cursorPositionChanged())); textEditMsg->installEventFilter(this); } void CommitImpl::closeEvent(QCloseEvent*) { QVector v(1, splitter); QGit::saveGeometrySetting(CMT_GEOM_KEY, this, &v); } void CommitImpl::contextMenuPopup(const QPoint& pos) { QMenu* contextMenu = new QMenu(this); QAction* a = contextMenu->addAction("Select All"); connect(a, SIGNAL(triggered()), this, SLOT(checkAll())); a = contextMenu->addAction("Unselect All"); connect(a, SIGNAL(triggered()), this, SLOT(unCheckAll())); contextMenu->popup(mapToGlobal(pos)); } void CommitImpl::checkAll() { checkUncheck(true); } void CommitImpl::unCheckAll() { checkUncheck(false); } void CommitImpl::checkUncheck(bool checkAll) { QTreeWidgetItemIterator it(treeWidgetFiles); while (*it) { (*it)->setCheckState(0, checkAll ? Qt::Checked : Qt::Unchecked); ++it; } } bool CommitImpl::getFiles(SList selFiles) { // check for files to commit selFiles.clear(); QTreeWidgetItemIterator it(treeWidgetFiles); while (*it) { if ((*it)->checkState(0) == Qt::Checked) selFiles.append((*it)->text(0)); ++it; } return !selFiles.isEmpty(); } void CommitImpl::warnNoFiles() { QMessageBox::warning(this, "Commit changes - QGit", "Sorry, no files are selected for updating.", QMessageBox::Ok, QMessageBox::NoButton); } bool CommitImpl::checkFiles(SList selFiles) { if (getFiles(selFiles)) return true; warnNoFiles(); return false; } bool CommitImpl::checkMsg(QString& msg) { msg = textEditMsg->toPlainText(); msg.remove(QRegExp("(^|\\n)\\s*#[^\\n]*")); // strip comments msg.replace(QRegExp("[ \\t\\r\\f\\v]+\\n"), "\n"); // strip line trailing cruft msg = msg.trimmed(); if (msg.isEmpty()) { QMessageBox::warning(this, "Commit changes - QGit", "Sorry, I don't want an empty message.", QMessageBox::Ok, QMessageBox::NoButton); return false; } // split subject from message body QString subj(msg.section('\n', 0, 0, QString::SectionIncludeTrailingSep)); QString body(msg.section('\n', 1).trimmed()); msg = subj + '\n' + body + '\n'; return true; } bool CommitImpl::checkPatchName(QString& patchName) { bool ok; patchName = patchName.simplified(); patchName.replace(' ', "_"); patchName = QInputDialog::getText(this, "Create new patch - QGit", "Enter patch name:", QLineEdit::Normal, patchName, &ok); if (!ok || patchName.isEmpty()) return false; QString tmp(patchName.trimmed()); if (patchName != tmp.remove(' ')) QMessageBox::warning(this, "Create new patch - QGit", "Sorry, control " "characters or spaces\n are not allowed in patch name."); else if (git->isPatchName(patchName)) QMessageBox::warning(this, "Create new patch - QGit", "Sorry, patch name " "already exists.\nPlease choose a different name."); else return true; return false; } bool CommitImpl::checkConfirm(SCRef msg, SCRef patchName, SCList selFiles, bool amend) { // QTextCodec* tc = QTextCodec::codecForCStrings(); // QTextCodec::setCodecForCStrings(0); // set temporary Latin-1 // NOTEME: i18n-ugly QString whatToDo = amend ? (git->isStGITStack() ? "refresh top patch with" : "amend last commit with") : (git->isStGITStack() ? "create a new patch with" : "commit"); QString text("Do you want to " + whatToDo); bool const fullList = selFiles.size() < 20; if (fullList) text.append(" the following file(s)?\n\n" + selFiles.join("\n") + "\n\nwith the message:\n\n"); else text.append(" those " + QString::number(selFiles.size()) + " files the with the message:\n\n"); text.append(msg); if (git->isStGITStack()) text.append("\n\nAnd patch name: " + patchName); // QTextCodec::setCodecForCStrings(tc); QMessageBox msgBox(this); msgBox.setWindowTitle("Commit changes - QGit"); msgBox.setText(text); if (!fullList) msgBox.setDetailedText(selFiles.join("\n")); msgBox.setStandardButtons(QMessageBox::Yes | QMessageBox::No); msgBox.setDefaultButton(QMessageBox::Yes); return msgBox.exec() != QMessageBox::No; } void CommitImpl::pushButtonSettings_clicked() { SettingsImpl setView(this, git, 3); setView.exec(); } void CommitImpl::pushButtonCancel_clicked() { close(); } void CommitImpl::pushButtonCommit_clicked() { QStringList selFiles; // retrieve selected files if (!checkFiles(selFiles)) return; QString msg; // check for commit message and strip comments if (!checkMsg(msg)) return; QString patchName(msg.section('\n', 0, 0)); // the subject if (git->isStGITStack() && !checkPatchName(patchName)) return; // ask for confirmation if (!checkConfirm(msg, patchName, selFiles, !Git::optAmend)) return; // ok, let's go QApplication::setOverrideCursor(QCursor(Qt::WaitCursor)); EM_PROCESS_EVENTS; // to close message box bool ok; if (git->isStGITStack()) ok = git->stgCommit(selFiles, msg, patchName, !Git::optFold); else ok = git->commitFiles(selFiles, msg, !Git::optAmend); lastMsgBeforeError = (ok ? "" : msg); QApplication::restoreOverrideCursor(); hide(); emit changesCommitted(ok); close(); } void CommitImpl::pushButtonAmend_clicked() { QStringList selFiles; // retrieve selected files getFiles(selFiles); // FIXME: If there are no files AND no changes to message, we should not // commit. Disabling the commit button in such case might be preferable. QString msg(textEditMsg->toPlainText()); if (msg == origMsg && selFiles.isEmpty()) { warnNoFiles(); return; } if (msg == origMsg && git->isStGITStack()) msg = ""; else if (!checkMsg(msg)) // We are going to replace the message, so it better isn't empty return; // ask for confirmation // FIXME: We don't need patch name for refresh, do we? if (!checkConfirm(msg, "", selFiles, Git::optAmend)) return; // ok, let's go QApplication::setOverrideCursor(QCursor(Qt::WaitCursor)); EM_PROCESS_EVENTS; // to close message box bool ok; if (git->isStGITStack()) ok = git->stgCommit(selFiles, msg, "", Git::optFold); else ok = git->commitFiles(selFiles, msg, Git::optAmend); QApplication::restoreOverrideCursor(); hide(); emit changesCommitted(ok); close(); } void CommitImpl::pushButtonUpdateCache_clicked() { QStringList selFiles; if (!checkFiles(selFiles)) return; bool ok = git->updateIndex(selFiles); QApplication::restoreOverrideCursor(); emit changesCommitted(ok); close(); } void CommitImpl::textEditMsg_cursorPositionChanged() { int col_pos, line_pos; computePosition(col_pos, line_pos); QString lineNumber = QString("Line: %1 Col: %2") .arg(line_pos + 1).arg(col_pos + 1); textLabelLineCol->setText(lineNumber); } void CommitImpl::computePosition(int &col_pos, int &line_pos) { QRect r = textEditMsg->cursorRect(); int vs = textEditMsg->verticalScrollBar()->value(); int hs = textEditMsg->horizontalScrollBar()->value(); // when in start position r.x() = -r.width() / 2 col_pos = (r.x() + hs + r.width() / 2) / ofsX; line_pos = (r.y() + vs) / ofsY; } bool CommitImpl::eventFilter(QObject* obj, QEvent* event) { if (obj == textEditMsg) { if (event->type() == QEvent::KeyPress) { QKeyEvent* keyEvent = static_cast(event); if (( keyEvent->key() == Qt::Key_Return || keyEvent->key() == Qt::Key_Enter ) && keyEvent->modifiers() & Qt::ControlModifier) { QMetaObject::invokeMethod(pushButtonOk, "clicked", Qt::QueuedConnection); return true; } } return false; } return QObject::eventFilter(obj, event); } qgit-2.7/src/commitimpl.h000066400000000000000000000022731305655150700154530ustar00rootroot00000000000000/* Author: Marco Costalba (C) 2005-2007 Copyright: See COPYING file that comes with this distribution */ #ifndef COMMITIMPL_H #define COMMITIMPL_H #include "ui_commit.h" #include "common.h" class Git; class CommitImpl : public QWidget, public Ui_CommitBase { Q_OBJECT public: explicit CommitImpl(Git* g, bool amend); signals: void changesCommitted(bool); public slots: virtual void closeEvent(QCloseEvent*); void pushButtonCommit_clicked(); void pushButtonAmend_clicked(); void pushButtonCancel_clicked(); void pushButtonUpdateCache_clicked(); void pushButtonSettings_clicked(); void textEditMsg_cursorPositionChanged(); private slots: void contextMenuPopup(const QPoint&); void checkAll(); void unCheckAll(); private: void checkUncheck(bool checkAll); bool getFiles(SList selFiles); void warnNoFiles(); bool checkFiles(SList selFiles); bool checkMsg(QString& msg); bool checkPatchName(QString& patchName); bool checkConfirm(SCRef msg, SCRef patchName, SCList selFiles, bool amend); void computePosition(int &col_pos, int &line_pos); bool eventFilter(QObject* obj, QEvent* event); Git* git; QString origMsg; int ofsX, ofsY; static QString lastMsgBeforeError; }; #endif qgit-2.7/src/common.cpp000066400000000000000000000171411305655150700151240ustar00rootroot00000000000000/* Description: interface to git programs Author: Marco Costalba (C) 2005-2007 Copyright: See COPYING file that comes with this distribution */ #include #include #include "common.h" const QString Rev::mid(int start, int len) const { // warning no sanity check is done on arguments const char* data = ba.constData(); return QString::fromLocal8Bit(data + start, len); } const QString Rev::midSha(int start, int len) const { // warning no sanity check is done on arguments const char* data = ba.constData(); return QString::fromLatin1(data + start, len); // faster then formAscii } const ShaString Rev::parent(int idx) const { return ShaString(ba.constData() + shaStart + 41 + 41 * idx); } const QStringList Rev::parents() const { QStringList p; int idx = shaStart + 41; for (int i = 0; i < parentsCnt; i++) { p.append(midSha(idx, 40)); idx += 41; } return p; } int Rev::indexData(bool quick, bool withDiff) const { /* This is what 'git log' produces: - a possible one line with "Final output:\n" in case of --early-output option - one line with "log size" + len of this record - one line with boundary info + sha + an arbitrary amount of parent's sha - one line with committer name + e-mail - one line with author name + e-mail - one line with author date as unix timestamp - zero or more non blank lines with other info, as the encoding FIXME - one blank line - zero or one line with log title - zero or more lines with log message - zero or more lines with diff content (only for file history) - a terminating '\0' */ static int error = -1; static int shaLength = 40; // from git ref. spec. static int shaEndlLength = shaLength + 1; // an sha key + \n static int shaXEndlLength = shaLength + 2; // an sha key + X marker + \n static char finalOutputMarker = 'F'; // marks the beginning of "Final output" string static char logSizeMarker = 'l'; // marks the beginning of "log size" string static int logSizeStrLength = 9; // "log size" static int asciiPosOfZeroChar = 48; // char "0" has value 48 in ascii table const int last = ba.size() - 1; int logSize = 0, idx = start; int logEnd, revEnd; // direct access is faster then QByteArray.at() const char* data = ba.constData(); char* fixup = const_cast(data); // to build '\0' terminating strings if (start + shaXEndlLength > last) // at least sha header must be present return -1; if (data[start] == finalOutputMarker) // "Final output", let caller handle this return (ba.indexOf('\n', start) != -1 ? -2 : -1); // parse 'log size xxx\n' if present -- from git ref. spec. if (data[idx] == logSizeMarker) { idx += logSizeStrLength; // move idx to beginning of log size value // parse log size value int digit; while ((digit = data[idx++]) != '\n') logSize = logSize * 10 + digit - asciiPosOfZeroChar; } // idx points to the boundary information, which has the same length as an sha header. if (++idx + shaXEndlLength > last) return error; shaStart = idx; // ok, now shaStart is valid but msgSize could be still 0 if not available logEnd = shaStart - 1 + logSize; if (logEnd > last) return error; idx += shaLength; // now points to 'X' place holder fixup[idx] = '\0'; // we want sha to be a '\0' terminated ascii string parentsCnt = 0; if (data[idx + 2] == '\n') // initial revision ++idx; else do { parentsCnt++; idx += shaEndlLength; if (idx + 1 >= last) break; fixup[idx] = '\0'; // we want parents '\0' terminated } while (data[idx + 1] != '\n'); ++idx; // now points to the trailing '\n' of sha line // check for !msgSize if (withDiff || !logSize) { revEnd = (logEnd > idx) ? logEnd - 1: idx; revEnd = ba.indexOf('\0', revEnd + 1); if (revEnd == -1) return -1; } else revEnd = logEnd; if (revEnd > last) // after this point we know to have the whole record return error; // ok, now revEnd is valid but logEnd could be not if !logSize // in case of diff we are sure content will be consumed so // we go all the way if (quick && !withDiff) return ++revEnd; // commiter comStart = ++idx; idx = ba.indexOf('\n', idx); // committer line end if (idx == -1) { dbs("ASSERT in indexData: unexpected end of data"); return -1; } // author autStart = ++idx; idx = ba.indexOf('\n', idx); // author line end if (idx == -1) { dbs("ASSERT in indexData: unexpected end of data"); return -1; } // author date in Unix format (seconds since epoch) autDateStart = ++idx; idx = ba.indexOf('\n', idx); // author date end without '\n' if (idx == -1) { dbs("ASSERT in indexData: unexpected end of data"); return -1; } // if no error, point to trailing \n ++idx; diffStart = diffLen = 0; if (withDiff) { diffStart = logSize ? logEnd : ba.indexOf("\ndiff ", idx); if (diffStart != -1 && diffStart < revEnd) diffLen = revEnd - ++diffStart; else diffStart = 0; } if (!logSize) logEnd = diffStart ? diffStart : revEnd; // ok, now logEnd is valid and we can handle the log sLogStart = idx; if (logEnd < sLogStart) { // no shortlog no longLog sLogStart = sLogLen = 0; lLogStart = lLogLen = 0; } else { lLogStart = ba.indexOf('\n', sLogStart); if (lLogStart != -1 && lLogStart < logEnd - 1) { sLogLen = lLogStart - sLogStart; // skip sLog trailing '\n' lLogLen = logEnd - lLogStart; // include heading '\n' in long log } else { // no longLog sLogLen = logEnd - sLogStart; if (data[sLogStart + sLogLen - 1] == '\n') sLogLen--; // skip trailing '\n' if any lLogStart = lLogLen = 0; } } indexed = true; return ++revEnd; } /** * RevFile streaming out */ const RevFile& RevFile::operator>>(QDataStream& stream) const { stream << pathsIdx; // skip common case of only modified files bool isEmpty = onlyModified; stream << (quint32)isEmpty; if (!isEmpty) stream << status; // skip common case of just one parent isEmpty = (mergeParent.isEmpty() || mergeParent.last() == 1); stream << (quint32)isEmpty; if (!isEmpty) stream << mergeParent; // skip common case of no rename/copies isEmpty = extStatus.isEmpty(); stream << (quint32)isEmpty; if (!isEmpty) stream << extStatus; return *this; } /** * RevFile streaming in */ RevFile& RevFile::operator<<(QDataStream& stream) { stream >> pathsIdx; bool isEmpty; quint32 tmp; stream >> tmp; onlyModified = (bool)tmp; if (!onlyModified) stream >> status; stream >> tmp; isEmpty = (bool)tmp; if (!isEmpty) stream >> mergeParent; stream >> tmp; isEmpty = (bool)tmp; if (!isEmpty) stream >> extStatus; return *this; } QString qt4and5escaping(QString toescape) { #if QT_VERSION >= 0x050000 return toescape.toHtmlEscaped(); #else return Qt::escape(toescape); #endif } qgit-2.7/src/common.h000066400000000000000000000345471305655150700146020ustar00rootroot00000000000000/* Author: Marco Costalba (C) 2005-2007 Copyright: See COPYING file that comes with this distribution */ #ifndef COMMON_H #define COMMON_H #include #include #include #include #include #include #include #include /* QVariant does not support size_t type used in Qt containers, this is a problem on 64bit systems where size_t != uint and when using debug macros on size_t variables, as example dbg(vector.count()), a compile error occurs. Workaround this using a function template and a specialization. Function _valueOf() is used by debug macros */ template inline const QString _valueOf(const T& x) { return QVariant(x).toString(); } template<> inline const QString _valueOf(const QStringList& x) { return x.join(" "); } inline const QString& _valueOf(const QString& x) { return x; } inline const QString _valueOf(size_t x) { return QString::number((uint)x); } // some debug macros #define constlatin(x) (_valueOf(x).toLatin1().constData()) #define dbg(x) qDebug(#x " is <%s>", constlatin(x)) #define dbs(x) qDebug(constlatin(x), "") #define dbp(s, x) qDebug(constlatin(_valueOf(s).arg(x)), "") #define db1 qDebug("Mark Nr. 1") #define db2 qDebug("Mark Nr. 2") #define db3 qDebug("Mark Nr. 3") #define db4 qDebug("Mark Nr. 4") #define dbStart dbs("Starting timer..."); QTime _t; _t.start() #define dbRestart dbp("Elapsed time is %1 ms", _t.restart()) // some syntactic sugar #define FOREACH(type, i, c) for (type::const_iterator i((c).constBegin()), \ _e##i##_((c).constEnd()); i != _e##i##_; ++i) #define FOREACH_SL(i, c) FOREACH(QStringList, i, c) class QDataStream; class QProcess; class QSplitter; class QWidget; class ShaString; // type shortcuts typedef const QString& SCRef; typedef QStringList& SList; typedef const QStringList& SCList; typedef QVector StrVect; typedef QVector ShaVect; typedef QSet ShaSet; uint qHash(const ShaString&); // optimized custom hash for sha strings namespace QGit { // minimum git version required extern const QString GIT_VERSION; // tab pages enum TabType { TAB_REV, TAB_PATCH, TAB_FILE }; // graph elements enum LaneType { EMPTY, ACTIVE, NOT_ACTIVE, MERGE_FORK, MERGE_FORK_R, MERGE_FORK_L, JOIN, JOIN_R, JOIN_L, HEAD, HEAD_R, HEAD_L, TAIL, TAIL_R, TAIL_L, CROSS, CROSS_EMPTY, INITIAL, BRANCH, UNAPPLIED, APPLIED, BOUNDARY, BOUNDARY_C, // corresponds to MERGE_FORK BOUNDARY_R, // corresponds to MERGE_FORK_R BOUNDARY_L, // corresponds to MERGE_FORK_L LANE_TYPES_NUM }; const int COLORS_NUM = 8; // graph helpers inline bool isHead(int x) { return (x == HEAD || x == HEAD_R || x == HEAD_L); } inline bool isTail(int x) { return (x == TAIL || x == TAIL_R || x == TAIL_L); } inline bool isJoin(int x) { return (x == JOIN || x == JOIN_R || x == JOIN_L); } inline bool isFreeLane(int x) { return (x == NOT_ACTIVE || x == CROSS || isJoin(x)); } inline bool isBoundary(int x) { return (x == BOUNDARY || x == BOUNDARY_C || x == BOUNDARY_R || x == BOUNDARY_L); } inline bool isMerge(int x) { return (x == MERGE_FORK || x == MERGE_FORK_R || x == MERGE_FORK_L || isBoundary(x)); } inline bool isActive(int x) { return (x == ACTIVE || x == INITIAL || x == BRANCH || isMerge(x)); } // custom events enum EventType { ERROR_EV = 65432, POPUP_LIST_EV = 65433, POPUP_FILE_EV = 65434, POPUP_TREE_EV = 65435, MSG_EV = 65436, ANN_PRG_EV = 65437, UPD_DM_EV = 65438, UPD_DM_MST_EV = 65439 }; // list views columns enum ColumnType { GRAPH_COL = 0, ANN_ID_COL = 1, LOG_COL = 2, AUTH_COL = 3, TIME_COL = 4, COMMIT_COL = 97, // dummy col used for sha searching LOG_MSG_COL = 98, // dummy col used for log messages searching SHA_MAP_COL = 99 // dummy col used when filter output is a set of matching sha }; inline bool isInfoCol(int x) { return (x == TIME_COL || x == LOG_COL || x == AUTH_COL); } // default list view widths const int DEF_GRAPH_COL_WIDTH = 80; const int DEF_LOG_COL_WIDTH = 500; const int DEF_AUTH_COL_WIDTH = 230; const int DEF_TIME_COL_WIDTH = 160; // colors extern const QColor BROWN; extern const QColor ORANGE; extern const QColor DARK_ORANGE; extern const QColor LIGHT_ORANGE; extern const QColor LIGHT_BLUE; extern const QColor PURPLE; extern const QColor DARK_GREEN; // initialized at startup according to system wide settings extern QColor ODD_LINE_COL; extern QColor EVEN_LINE_COL; extern QFont STD_FONT; extern QFont TYPE_WRITER_FONT; extern QString GIT_DIR; // patches drag and drop extern const QString PATCHES_DIR; extern const QString PATCHES_NAME; // git index parameters extern const QByteArray ZERO_SHA_BA; extern const ShaString ZERO_SHA_RAW; extern const QString ZERO_SHA; extern const QString CUSTOM_SHA; extern const QString ALL_MERGE_FILES; // settings keys extern const QString ORG_KEY; extern const QString APP_KEY; extern const QString GIT_DIR_KEY; extern const QString PATCH_DIR_KEY; extern const QString FMT_P_OPT_KEY; extern const QString AM_P_OPT_KEY; extern const QString STD_FNT_KEY; extern const QString TYPWRT_FNT_KEY; extern const QString FLAGS_KEY; extern const QString CON_GEOM_KEY; extern const QString CMT_GEOM_KEY; extern const QString MAIN_GEOM_KEY; extern const QString REV_GEOM_KEY; extern const QString CMT_TEMPL_KEY; extern const QString CMT_ARGS_KEY; extern const QString RANGE_FROM_KEY; extern const QString RANGE_TO_KEY; extern const QString RANGE_OPT_KEY; extern const QString EX_KEY; extern const QString EX_PER_DIR_KEY; extern const QString EXT_DIFF_KEY; extern const QString EXT_EDITOR_KEY; extern const QString REC_REP_KEY; extern const QString ACT_LIST_KEY; extern const QString ACT_GEOM_KEY; extern const QString ACT_GROUP_KEY; extern const QString ACT_TEXT_KEY; extern const QString ACT_FLAGS_KEY; // settings default values extern const QString CMT_TEMPL_DEF; extern const QString EX_DEF; extern const QString EX_PER_DIR_DEF; extern const QString EXT_DIFF_DEF; extern const QString EXT_EDITOR_DEF; // settings booleans enum FlagType { MSG_ON_NEW_F = 1 << 0, ACT_REFRESH_F = 1 << 1, NUMBERS_F = 1 << 2, LOG_DIFF_TAB_F = 1 << 3, ACT_CMD_LINE_F = 1 << 4, DIFF_INDEX_F = 1 << 5, SIGN_PATCH_F = 1 << 6, SIGN_CMT_F = 1 << 7, VERIFY_CMT_F = 1 << 8, SMART_LBL_F = 1 << 9, REL_DATE_F = 1 << 10, ALL_BRANCHES_F = 1 << 11, WHOLE_HISTORY_F = 1 << 12, RANGE_SELECT_F = 1 << 13, REOPEN_REPO_F = 1 << 14, USE_CMT_MSG_F = 1 << 15, OPEN_IN_EDITOR_F = 1 << 16 }; const int FLAGS_DEF = USE_CMT_MSG_F | RANGE_SELECT_F | SMART_LBL_F | VERIFY_CMT_F | SIGN_PATCH_F | LOG_DIFF_TAB_F | MSG_ON_NEW_F; // ShaString helpers const ShaString toTempSha(const QString&); // use as argument only, see definition const ShaString toPersistentSha(const QString&, QVector&); // settings helpers uint flags(SCRef flagsVariable); bool testFlag(uint f, SCRef fv = FLAGS_KEY); void setFlag(uint f, bool b, SCRef fv = FLAGS_KEY); // tree view icons helpers void initMimePix(); void freeMimePix(); const QPixmap* mimePix(SCRef fileName); // geometry settings helers typedef QVector splitVect; void saveGeometrySetting(SCRef name, QWidget* w = NULL, splitVect* svPtr = NULL); void restoreGeometrySetting(SCRef name, QWidget* w = NULL, splitVect* svPtr = NULL); // misc helpers bool stripPartialParaghraps(const QByteArray& src, QString* dst, QString* prev); bool writeToFile(SCRef fileName, SCRef data, bool setExecutable = false); bool writeToFile(SCRef fileName, const QByteArray& data, bool setExecutable = false); bool readFromFile(SCRef fileName, QString& data); bool startProcess(QProcess* proc, SCList args, SCRef buf = "", bool* winShell = NULL); // cache file const uint C_MAGIC = 0xA0B0C0D0; const int C_VERSION = 15; extern const QString BAK_EXT; extern const QString C_DAT_FILE; // misc const int MAX_DICT_SIZE = 100003; // must be a prime number see QDict docs const int MAX_MENU_ENTRIES = 20; const int MAX_RECENT_REPOS = 7; extern const QString QUOTE_CHAR; extern const QString SCRIPT_EXT; } class ShaString : public QLatin1String { public: inline ShaString() : QLatin1String(NULL) {} inline ShaString(const ShaString& sha) : QLatin1String(sha.latin1()) {} inline explicit ShaString(const char* sha) : QLatin1String(sha) {} inline bool operator!=(const ShaString& o) const { return !operator==(o); } inline bool operator==(const ShaString& o) const { return (latin1() == o.latin1()) || !qstrcmp(latin1(), o.latin1()); } }; class Rev { // prevent implicit C++ compiler defaults Rev(); Rev(const Rev&); Rev& operator=(const Rev&); public: Rev(const QByteArray& b, uint s, int idx, int* next, bool withDiff) : orderIdx(idx), ba(b), start(s) { indexed = isDiffCache = isApplied = isUnApplied = false; descRefsMaster = ancRefsMaster = descBrnMaster = -1; *next = indexData(true, withDiff); } bool isBoundary() const { return (ba.at(shaStart - 1) == '-'); } uint parentsCount() const { return parentsCnt; } const ShaString parent(int idx) const; const QStringList parents() const; const ShaString sha() const { return ShaString(ba.constData() + shaStart); } const QString committer() const { setup(); return mid(comStart, autStart - comStart - 1); } const QString author() const { setup(); return mid(autStart, autDateStart - autStart - 1); } const QString authorDate() const { setup(); return mid(autDateStart, 10); } const QString shortLog() const { setup(); return mid(sLogStart, sLogLen); } const QString longLog() const { setup(); return mid(lLogStart, lLogLen); } const QString diff() const { setup(); return mid(diffStart, diffLen); } QVector lanes, children; QVector descRefs; // list of descendant refs index, normally tags QVector ancRefs; // list of ancestor refs index, normally tags QVector descBranches; // list of descendant branches index int descRefsMaster; // in case of many Rev have the same descRefs, ancRefs or int ancRefsMaster; // descBranches these are stored only once in a Rev pointed int descBrnMaster; // by corresponding index xxxMaster int orderIdx; private: inline void setup() const { if (!indexed) indexData(false, false); } int indexData(bool quick, bool withDiff) const; const QString mid(int start, int len) const; const QString midSha(int start, int len) const; const QByteArray& ba; // reference here! const int start; mutable int parentsCnt, shaStart, comStart, autStart, autDateStart; mutable int sLogStart, sLogLen, lLogStart, lLogLen, diffStart, diffLen; mutable bool indexed; public: bool isDiffCache, isApplied, isUnApplied; // put here to optimize padding }; typedef QHash RevMap; // faster then a map class RevFile { friend class Cache; // to directly load status friend class Git; // Status information is splitted in a flags vector and in a string // vector in 'status' are stored flags according to the info returned // by 'git diff-tree' without -C option. // In case of a working directory file an IN_INDEX flag is or-ed togheter in // case file is present in git index. // If file is renamed or copied an entry in 'extStatus' stores the // value returned by 'git diff-tree -C' plus source and destination // files info. // When status of all the files is 'modified' then onlyModified is // set, this let us to do some optimization in this common case bool onlyModified; QVector status; QVector extStatus; // prevent implicit C++ compiler defaults RevFile(const RevFile&); RevFile& operator=(const RevFile&); public: enum StatusFlag { MODIFIED = 1, DELETED = 2, NEW = 4, RENAMED = 8, COPIED = 16, UNKNOWN = 32, IN_INDEX = 64, ANY = 127 }; RevFile() : onlyModified(true) {} /* This QByteArray keeps indices in some dir and names vectors, * defined outside RevFile. Paths are splitted in dir and file * name, first all the dirs are listed then the file names to * achieve a better compression when saved to disk. * A single QByteArray is used instead of two vectors because it's * much faster to load from disk when using a QDataStream */ QByteArray pathsIdx; int dirAt(uint idx) const { return ((int*)pathsIdx.constData())[idx]; } int nameAt(uint idx) const { return ((int*)pathsIdx.constData())[count() + idx]; } QVector mergeParent; // helper functions int count() const { return pathsIdx.size() / ((int)sizeof(int) * 2); } bool statusCmp(int idx, StatusFlag sf) const { return ((onlyModified ? MODIFIED : status.at(idx)) & sf); } const QString extendedStatus(int idx) const { /* rf.extStatus has size equal to position of latest copied/renamed file, that could be lower then count(), so we have to explicitly check for an out of bound condition. */ return (!extStatus.isEmpty() && idx < extStatus.count() ? extStatus.at(idx) : ""); } const RevFile& operator>>(QDataStream&) const; RevFile& operator<<(QDataStream&); }; typedef QHash RevFileMap; class FileAnnotation { public: explicit FileAnnotation(int id) : isValid(false), annId(id) {} FileAnnotation() : isValid(false) {} QStringList lines; bool isValid; int annId; QString fileSha; }; typedef QHash AnnotateHistory; class BaseEvent: public QEvent { public: BaseEvent(SCRef d, int id) : QEvent((QEvent::Type)id), payLoad(d) {} const QString myData() const { return payLoad; } private: const QString payLoad; // passed by copy }; #define DEF_EVENT(X, T) class X : public BaseEvent { public: \ explicit X (SCRef d) : BaseEvent(d, T) {} } DEF_EVENT(MessageEvent, QGit::MSG_EV); DEF_EVENT(AnnotateProgressEvent, QGit::ANN_PRG_EV); class DeferredPopupEvent : public BaseEvent { public: DeferredPopupEvent(SCRef msg, int type) : BaseEvent(msg, type) {} }; class MainExecErrorEvent : public BaseEvent { public: MainExecErrorEvent(SCRef c, SCRef e) : BaseEvent("", QGit::ERROR_EV), cmd(c), err(e) {} const QString command() const { return cmd; } const QString report() const { return err; } private: const QString cmd, err; }; #endif QString qt4and5escaping(QString toescape); qgit-2.7/src/config.h000066400000000000000000000012111305655150700145350ustar00rootroot00000000000000/* config.h. Generated by configure. */ /* config.h.in. Generated from configure.ac by autoheader. */ /* Name of package */ #define PACKAGE "qgit" /* Define to the address where bug reports for this package should be sent. */ #define PACKAGE_BUGREPORT "" /* Define to the full name of this package. */ #define PACKAGE_NAME "qgit" /* Define to the full name and version of this package. */ #define PACKAGE_STRING "qgit" /* Define to the one symbol short name of this package. */ #define PACKAGE_TARNAME "qgit" /* Define to the version of this package. */ #define PACKAGE_VERSION "2.7" /* Version number of package */ #define VERSION "2.7" qgit-2.7/src/console.ui000066400000000000000000000567751305655150700151510ustar00rootroot00000000000000 Console 0 0 724 442 Action output window - QGit :/icons/resources/openterm.png 9 6 0 6 5 1 0 0 16 70 QFrame::StyledPanel QFrame::Plain 9 6 0 6 5 5 3 0 Sans Serif 9 75 false true false false Shows action definition ... Qt::PlainText Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop true Qt::Horizontal 40 20 1 0 0 0 Click to terminate action execution &Terminate :/icons/resources/cancel.png false true QFrame::StyledPanel QFrame::Plain 9 6 0 0 0 221 223 228 255 255 255 255 255 255 85 85 85 199 199 199 85 255 0 255 255 255 0 0 0 0 0 0 239 239 239 0 0 0 103 141 178 255 255 255 0 0 238 82 24 139 232 232 232 0 0 0 221 223 228 255 255 255 255 255 255 85 85 85 199 199 199 85 255 0 255 255 255 0 0 0 0 0 0 239 239 239 0 0 0 103 141 178 255 255 255 0 0 238 82 24 139 232 232 232 128 128 128 221 223 228 255 255 255 255 255 255 85 85 85 199 199 199 199 199 199 255 255 255 128 128 128 239 239 239 239 239 239 0 0 0 86 117 148 255 255 255 0 0 238 82 24 139 232 232 232 false false true false 0 6 Qt::Horizontal 531 20 1 0 0 0 Click to close window. Warning action will not be terminated if running &Ok true Terminate pushButtonTerminate pushButtonOk textEditOutput pushButtonTerminate clicked() Console pushButtonTerminate_clicked() 658 49 361 220 pushButtonOk clicked() Console pushButtonOk_clicked() 658 404 361 220 qgit-2.7/src/consoleimpl.cpp000066400000000000000000000047311305655150700161610ustar00rootroot00000000000000/* Description: stdout viewer Author: Marco Costalba (C) 2006-2007 Copyright: See COPYING file that comes with this distribution */ #include #include #include #include "myprocess.h" #include "git.h" #include "consoleimpl.h" ConsoleImpl::ConsoleImpl(const QString& nm, Git* g) : git(g), actionName(nm) { setAttribute(Qt::WA_DeleteOnClose); setupUi(this); textEditOutput->setFont(QGit::TYPE_WRITER_FONT); QFont f = textLabelCmd->font(); f.setBold(true); textLabelCmd->setFont(f); setWindowTitle("\'" + actionName + "\' output window - QGit"); QGit::restoreGeometrySetting(QGit::CON_GEOM_KEY, this); } void ConsoleImpl::typeWriterFontChanged() { QTextEdit* te = textEditOutput; te->setFont(QGit::TYPE_WRITER_FONT); te->setPlainText(te->toPlainText()); te->moveCursor(QTextCursor::End); } void ConsoleImpl::pushButtonOk_clicked() { close(); } void ConsoleImpl::pushButtonTerminate_clicked() { git->cancelProcess(proc); procFinished(); } void ConsoleImpl::closeEvent(QCloseEvent* ce) { if (proc && proc->state() == QProcess::Running) if (QMessageBox::question(this, "Action output window - QGit", "Action is still running.\nAre you sure you want to close " "the window and leave the action running in background?", "&Yes", "&No", QString(), 1, 1) == 1) { ce->ignore(); return; } if (QApplication::overrideCursor()) QApplication::restoreOverrideCursor(); QGit::saveGeometrySetting(QGit::CON_GEOM_KEY, this); QMainWindow::closeEvent(ce); } bool ConsoleImpl::start(const QString& cmd) { statusBar()->showMessage("Executing \'" + actionName + "\' action..."); textLabelCmd->setText(cmd); if (cmd.indexOf('\n') < 0) proc = git->runAsync(cmd, this); else proc = git->runAsScript(cmd, this); // wrap multiline cmd in a script if (proc.isNull()) deleteLater(); else QApplication::setOverrideCursor(QCursor(Qt::WaitCursor)); return !proc.isNull(); } void ConsoleImpl::procReadyRead(const QByteArray& data) { QString newParagraph; if (QGit::stripPartialParaghraps(data, &newParagraph, &inpBuf)) // QTextEdit::append() adds a new paragraph, // i.e. inserts a LF if not already present. textEditOutput->append(newParagraph); } void ConsoleImpl::procFinished() { textEditOutput->append(inpBuf); inpBuf = ""; QApplication::restoreOverrideCursor(); statusBar()->showMessage("End of \'" + actionName + "\' execution."); pushButtonTerminate->setEnabled(false); emit customAction_exited(actionName); } qgit-2.7/src/consoleimpl.h000066400000000000000000000015131305655150700156210ustar00rootroot00000000000000/* Description: stdout viewer Author: Marco Costalba (C) 2006-2007 Copyright: See COPYING file that comes with this distribution */ #ifndef CONSOLEIMPL_H #define CONSOLEIMPL_H #include #include #include "ui_console.h" class MyProcess; class Git; class ConsoleImpl : public QMainWindow, Ui_Console { // we need a statusbar Q_OBJECT public: ConsoleImpl(const QString& nm, Git* g); bool start(const QString &cmd); signals: void customAction_exited(const QString& name); public slots: void typeWriterFontChanged(); void procReadyRead(const QByteArray& data); void procFinished(); protected slots: virtual void closeEvent(QCloseEvent* ce); void pushButtonTerminate_clicked(); void pushButtonOk_clicked(); private: Git* git; QString actionName; QPointer proc; QString inpBuf; }; #endif qgit-2.7/src/customaction.ui000066400000000000000000000365161305655150700162060ustar00rootroot00000000000000 CustomActionBase 0 0 564 492 Actions builder :/icons/resources/bookmark.png 0 5 5 5 5 6 0 0 0 0 0 0 QFrame::StyledPanel QFrame::Raised 6 9 9 9 9 15 0 0 0 0 &New :/icons/resources/bookmark_add.png Alt+N &Rename :/icons/resources/pencil.png Alt+R R&emove :/icons/resources/remove.png Alt+E Qt::LeftToRight Move &up :/icons/resources/1uparrow.png Alt+U Move &down :/icons/resources/1downarrow.png Alt+D 0 0 Qt::Horizontal QFrame::StyledPanel QFrame::Raised 6 9 9 9 9 6 0 0 0 0 Name listWidgetNames QFrame::StyledPanel QFrame::Raised 6 9 9 9 9 6 0 0 0 0 Insert commands to run: Insert commands to run. See handbook (F1) for some examples. false Inserting tokens of the form %lineedit:arguments=defaults% will open a dialog to enter arguments. Additionally, you can use variables $CURRENT_BRANCH and $SHA. If checked main view will be automatically refreshed when done Refresh &view at the end Alt+V 6 0 0 0 0 6 0 0 0 0 Qt::Horizontal QSizePolicy::Expanding 101 20 &Ok Alt+O true true pushButtonNew clicked() CustomActionBase pushButtonNew_clicked() 20 20 20 20 pushButtonRename clicked() CustomActionBase pushButtonRename_clicked() 20 20 20 20 pushButtonRemove clicked() CustomActionBase pushButtonRemove_clicked() 20 20 20 20 checkBoxRefreshAfterAction toggled(bool) CustomActionBase checkBoxRefreshAfterAction_toggled(bool) 20 20 20 20 pushButtonMoveUp clicked() CustomActionBase pushButtonMoveUp_clicked() 20 20 20 20 pushButtonMoveDown clicked() CustomActionBase pushButtonMoveDown_clicked() 20 20 20 20 pushButtonOk clicked() CustomActionBase pushButtonOk_clicked() 20 20 20 20 textEditAction textChanged() CustomActionBase textEditAction_textChanged() 395 239 330 231 listWidgetNames currentItemChanged(QListWidgetItem*,QListWidgetItem*) CustomActionBase listWidgetNames_currentItemChanged(QListWidgetItem*,QListWidgetItem*) 129 256 337 232 qgit-2.7/src/customactionimpl.cpp000066400000000000000000000127321305655150700172270ustar00rootroot00000000000000/* Description: custom action handling Author: Marco Costalba (C) 2006-2007 Copyright: See COPYING file that comes with this distribution */ #include #include #include #include "common.h" #include "customactionimpl.h" using namespace QGit; CustomActionImpl::CustomActionImpl() { setAttribute(Qt::WA_DeleteOnClose); setupUi(this); QSettings settings; restoreGeometry(settings.value(ACT_GEOM_KEY).toByteArray()); QStringList actions = settings.value(ACT_LIST_KEY).toStringList(); listWidgetNames->insertItems(0, actions); if (listWidgetNames->count()) listWidgetNames->setCurrentItem(listWidgetNames->item(0)); else listWidgetNames_currentItemChanged(NULL, NULL); } void CustomActionImpl::closeEvent(QCloseEvent* ce) { QSettings settings; settings.setValue(ACT_GEOM_KEY, saveGeometry()); QWidget::closeEvent(ce); } void CustomActionImpl::loadAction(const QString& name) { const QString flags(ACT_GROUP_KEY + name + ACT_FLAGS_KEY); checkBoxRefreshAfterAction->setChecked(testFlag(ACT_REFRESH_F, flags)); QSettings set; const QString& data(set.value(ACT_GROUP_KEY + name + ACT_TEXT_KEY, "").toString()); textEditAction->setPlainText(data); } void CustomActionImpl::removeAction(const QString& name) { QSettings set; set.remove(ACT_GROUP_KEY + name); } const QStringList CustomActionImpl::actions() { QStringList actionsList; QListWidgetItem* item; int row = 0; while ((item = listWidgetNames->item(row)) != 0) { actionsList.append(item->text()); row++; } return actionsList; } void CustomActionImpl::updateActions() { QSettings settings; settings.setValue(ACT_LIST_KEY, actions()); emit listChanged(actions()); } void CustomActionImpl::listWidgetNames_currentItemChanged(QListWidgetItem* item, QListWidgetItem*) { bool empty = (item == NULL); if (!empty) { loadAction(item->text()); listWidgetNames->scrollToItem(item); } else { textEditAction->clear(); if (checkBoxRefreshAfterAction->isChecked()) checkBoxRefreshAfterAction->toggle(); } textEditAction->setEnabled(!empty); checkBoxRefreshAfterAction->setEnabled(!empty); pushButtonRename->setEnabled(!empty); pushButtonRemove->setEnabled(!empty); pushButtonMoveUp->setEnabled(!empty && (item != listWidgetNames->item(0))); int lastRow = listWidgetNames->count() - 1; pushButtonMoveDown->setEnabled(!empty && (item != listWidgetNames->item(lastRow))); } bool CustomActionImpl::getNewName(QString& name, const QString& caption) { bool ok; const QString oldName = name; name = QInputDialog::getText(this, caption + " - QGit", "Enter action name:", QLineEdit::Normal, name, &ok); if (!ok || name.isEmpty() || name == oldName) return false; if (actions().contains(name)) { QMessageBox::warning(this, caption + " - QGit", "Sorry, action name " "already exists.\nPlease choose a different name."); return false; } return true; } void CustomActionImpl::pushButtonNew_clicked() { QString name; if (!getNewName(name, "Create new action")) return; QListWidgetItem* item = new QListWidgetItem(name); listWidgetNames->addItem(item); updateActions(); listWidgetNames->setCurrentItem(item); textEditAction->setPlainText(""); textEditAction->selectAll(); textEditAction->setFocus(); } void CustomActionImpl::pushButtonRename_clicked() { QListWidgetItem* item = listWidgetNames->currentItem(); if (!item || !item->isSelected()) return; QString newName(item->text()); if (!getNewName(newName, "Rename action")) return; const QString oldActionName(item->text()); item->setText(newName); updateActions(); listWidgetNames_currentItemChanged(item, item); loadAction(oldActionName); removeAction(oldActionName); } void CustomActionImpl::pushButtonRemove_clicked() { QListWidgetItem* item = listWidgetNames->currentItem(); if (!item || !item->isSelected()) return; removeAction(item->text()); delete item; updateActions(); if (!listWidgetNames->count()) listWidgetNames_currentItemChanged(NULL, NULL); } void CustomActionImpl::pushButtonMoveUp_clicked() { QListWidgetItem* item = listWidgetNames->currentItem(); int row = listWidgetNames->row(item); if (!item || row == 0) return; item = listWidgetNames->takeItem(row); listWidgetNames->insertItem(row - 1, item); updateActions(); listWidgetNames->setCurrentItem(item); } void CustomActionImpl::pushButtonMoveDown_clicked() { QListWidgetItem* item = listWidgetNames->currentItem(); int row = listWidgetNames->row(item); if (!item || row == listWidgetNames->count() - 1) return; item = listWidgetNames->takeItem(row); listWidgetNames->insertItem(row + 1, item); updateActions(); listWidgetNames->setCurrentItem(item); } void CustomActionImpl::textEditAction_textChanged() { QListWidgetItem* item = listWidgetNames->currentItem(); if (item) { QSettings s; QString key(ACT_GROUP_KEY + item->text() + ACT_TEXT_KEY); s.setValue(key, textEditAction->toPlainText()); } } void CustomActionImpl::checkBoxRefreshAfterAction_toggled(bool b) { QListWidgetItem* item = listWidgetNames->currentItem(); if (item) { QString flags(ACT_GROUP_KEY + item->text() + ACT_FLAGS_KEY); setFlag(ACT_REFRESH_F, b, flags); } } void CustomActionImpl::checkBoxAskArgs_toggled(bool b) { QListWidgetItem* item = listWidgetNames->currentItem(); if (item) { QString flags(ACT_GROUP_KEY + item->text() + ACT_FLAGS_KEY); setFlag(ACT_CMD_LINE_F, b, flags); } } void CustomActionImpl::pushButtonOk_clicked() { close(); } qgit-2.7/src/customactionimpl.h000066400000000000000000000020401305655150700166630ustar00rootroot00000000000000/* Description: custom action handling Author: Marco Costalba (C) 2006-2007 Copyright: See COPYING file that comes with this distribution */ #ifndef CUSTOMACTIONIMPL_H #define CUSTOMACTIONIMPL_H #include "ui_customaction.h" class CustomActionImpl : public QWidget, public Ui_CustomActionBase { Q_OBJECT public: CustomActionImpl(); signals: void listChanged(const QStringList&); protected slots: virtual void closeEvent(QCloseEvent*); void listWidgetNames_currentItemChanged(QListWidgetItem*, QListWidgetItem*); void pushButtonNew_clicked(); void pushButtonRename_clicked(); void pushButtonRemove_clicked(); void pushButtonMoveUp_clicked(); void pushButtonMoveDown_clicked(); void checkBoxRefreshAfterAction_toggled(bool); void checkBoxAskArgs_toggled(bool); void textEditAction_textChanged(); void pushButtonOk_clicked(); private: const QStringList actions(); void updateActions(); bool getNewName(QString& name, const QString& caption); void loadAction(const QString& name); void removeAction(const QString& name); }; #endif qgit-2.7/src/dataloader.cpp000066400000000000000000000203631305655150700157340ustar00rootroot00000000000000/* Description: async stream reader, used to load repository data on startup Author: Marco Costalba (C) 2005-2007 Copyright: See COPYING file that comes with this distribution */ #include #include #include "FileHistory.h" #include "git.h" #include "dataloader.h" #define GUI_UPDATE_INTERVAL 500 #define READ_BLOCK_SIZE 65535 class UnbufferedTemporaryFile : public QTemporaryFile { public: explicit UnbufferedTemporaryFile(QObject* p) : QTemporaryFile(p) {} bool unbufOpen() { return open(QIODevice::ReadOnly | QIODevice::Unbuffered); } }; DataLoader::DataLoader(Git* g, FileHistory* f) : QProcess(g), git(g), fh(f) { canceling = parsing = false; isProcExited = true; halfChunk = NULL; dataFile = NULL; loadedBytes = 0; guiUpdateTimer.setSingleShot(true); connect(git, SIGNAL(cancelAllProcesses()), this, SLOT(on_cancel())); connect(&guiUpdateTimer, SIGNAL(timeout()), this, SLOT(on_timeout())); } DataLoader::~DataLoader() { // avoid a Qt warning in case we are // destroyed while still running waitForFinished(1000); } void DataLoader::on_cancel(const FileHistory* f) { if (f == fh) on_cancel(); } void DataLoader::on_cancel() { if (!canceling) { // just once canceling = true; kill(); // SIGKILL (Unix and Mac), TerminateProcess (Windows) } } bool DataLoader::start(SCList args, SCRef wd, SCRef buf) { if (!isProcExited) { dbs("ASSERT in DataLoader::start(), called while processing"); return false; } isProcExited = false; setWorkingDirectory(wd); connect(this, SIGNAL(finished(int, QProcess::ExitStatus)), this, SLOT(on_finished(int, QProcess::ExitStatus))); if (!createTemporaryFile() || !QGit::startProcess(this, args, buf)) { deleteLater(); return false; } loadTime.start(); guiUpdateTimer.start(GUI_UPDATE_INTERVAL); return true; } void DataLoader::on_finished(int, QProcess::ExitStatus) { isProcExited = true; if (parsing && guiUpdateTimer.isActive()) dbs("ASSERT in DataLoader: timer active while parsing"); if (parsing == guiUpdateTimer.isActive() && !canceling) dbs("ASSERT in DataLoader: inconsistent timer"); if (guiUpdateTimer.isActive()) // no need to wait anymore guiUpdateTimer.start(1); } void DataLoader::on_timeout() { if (canceling) { deleteLater(); return; // we leave with guiUpdateTimer not active } parsing = true; // process could exit while we are processing so save the flag now bool lastBuffer = isProcExited; loadedBytes += readNewData(lastBuffer); emit newDataReady(fh); // inserting in list view is about 3% of total time if (lastBuffer) { emit loaded(fh, loadedBytes, loadTime.elapsed(), true, "", ""); deleteLater(); } else if (isProcExited) { // exited while parsing dbs("Exited while parsing!!!!"); guiUpdateTimer.start(1); } else guiUpdateTimer.start(GUI_UPDATE_INTERVAL); parsing = false; } void DataLoader::parseSingleBuffer(const QByteArray& ba) { if (ba.size() == 0 || canceling) return; int ofs = 0, newOfs, bz = ba.size(); /* Due to unknown reasons randomly first byte * of 'ba' is 0, this seems to happen only when * using QFile::read(), i.e. with temporary file * interface. Until we discover the real reason * workaround this skipping the bogus byte */ if (ba.at(0) == 0 && bz > 1 && !halfChunk) ofs++; while (bz - ofs > 0) { if (!halfChunk) { newOfs = git->addChunk(fh, ba, ofs); if (newOfs == -1) break; // half chunk detected ofs = newOfs; } else { // less then 1% of cases with READ_BLOCK_SIZE = 64KB int end = ba.indexOf('\0'); if (end == -1) // consecutives half chunks break; ofs = end + 1; baAppend(&halfChunk, ba.constData(), ofs); fh->rowData.append(halfChunk); addSplittedChunks(halfChunk); halfChunk = NULL; } } // save any remaining half chunk if (bz - ofs > 0) baAppend(&halfChunk, ba.constData() + ofs, bz - ofs); } void DataLoader::addSplittedChunks(const QByteArray* hc) { if (hc->at(hc->size() - 1) != 0) { dbs("ASSERT in DataLoader, bad half chunk"); return; } // do not assume we have only one chunk in hc int ofs = 0; while (ofs != -1 && ofs != (int)hc->size()) ofs = git->addChunk(fh, *hc, ofs); } void DataLoader::baAppend(QByteArray** baPtr, const char* ascii, int len) { if (*baPtr) // we cannot use QByteArray::append(const char*) // because 'ascii' is not '\0' terminating (*baPtr)->append(QByteArray::fromRawData(ascii, len)); else *baPtr = new QByteArray(ascii, len); } // *************** git interface facility dependant code ***************************** #ifdef USE_QPROCESS ulong DataLoader::readNewData(bool lastBuffer) { /* QByteArray copy c'tor uses shallow copy, but there is a deep copy in QProcess::readStdout(), from an internal buffers list to return value. Qt uses a select() to detect new data is ready, copies immediately the data to the heap with a read() and stores the pointer to new data in a pointer list, from qprocess_unix.cpp: const int basize = 4096; QByteArray *ba = new QByteArray(basize); n = ::read(fd, ba->data(), basize); buffer->append(ba); // added to a QPtrList pointer list When we call QProcess::readStdout() data from buffers pointed by the pointer list is memcpy() to the function return value, from qprocess.cpp: .... return buf->readAll(); // memcpy() here */ QByteArray* ba = new QByteArray(readAllStandardOutput()); if (lastBuffer) ba->append('\0'); // be sure stream is null terminated if (ba->size() == 0) { delete ba; return 0; } fh->rowData.append(ba); parseSingleBuffer(*ba); return ba->size(); } bool DataLoader::createTemporaryFile() { return true; } #else // temporary file as data exchange facility ulong DataLoader::readNewData(bool lastBuffer) { bool ok = dataFile && (dataFile->isOpen() || (dataFile->exists() && dataFile->unbufOpen())); if (!ok) return 0; ulong cnt = 0; qint64 readPos = dataFile->pos(); while (true) { // this is the ONLY deep copy involved in the whole loading // QFile::read() calls standard C read() function when // file is open with Unbuffered flag, or fread() otherwise QByteArray* ba = new QByteArray(); ba->resize(READ_BLOCK_SIZE); int len = static_cast(dataFile->read(ba->data(), READ_BLOCK_SIZE)); if (len <= 0) { delete ba; break; } else if (len < ba->size()) // unlikely ba->resize(len); // current read position must be updated manually, it's // not correctly incremented by read() if the producer // process has already finished readPos += len; dataFile->seek(readPos); cnt += len; fh->rowData.append(ba); parseSingleBuffer(*ba); // avoid reading small chunks if data producer is still running if (len < READ_BLOCK_SIZE && !lastBuffer) break; } if (lastBuffer) { // be sure stream is null terminated QByteArray* zb = new QByteArray(1, '\0'); fh->rowData.append(zb); parseSingleBuffer(*zb); } return cnt; } bool DataLoader::createTemporaryFile() { // redirect 'git log' output to a temporary file dataFile = new UnbufferedTemporaryFile(this); #ifndef Q_OS_WIN32 /* For performance reasons we would like to use a tmpfs filesystem if available, this is normally mounted under '/tmp' in Linux. According to Qt docs, a temporary file is placed in QDir::tempPath(), that should be system's temporary directory. On Unix/Linux systems this is usually /tmp; on Windows this is usually the path in the TEMP or TMP environment variable. But due to a bug in Qt 4.2 QDir::tempPath() is instead set to $HOME/tmp under Unix/Linux, that is not a tmpfs filesystem. So try to manually set the best directory for our temporary file. */ QDir dir("/tmp"); bool foundTmpDir = (dir.exists() && dir.isReadable()); if (foundTmpDir && dir.absolutePath() != QDir::tempPath()) { dataFile->setFileTemplate(dir.absolutePath() + "/qt_temp"); if (!dataFile->open()) { // test for write access delete dataFile; dataFile = new UnbufferedTemporaryFile(this); dbs("WARNING: directory '/tmp' is not writable, " "fallback on Qt default one, there could " "be a performance penalty."); } else dataFile->close(); } #endif if (!dataFile->open()) // to read the file name return false; setStandardOutputFile(dataFile->fileName()); dataFile->close(); return true; } #endif // USE_QPROCESS qgit-2.7/src/dataloader.h000066400000000000000000000025121305655150700153750ustar00rootroot00000000000000/* Author: Marco Costalba (C) 2005-2007 Copyright: See COPYING file that comes with this distribution */ #ifndef DATALOADER_H #define DATALOADER_H #include #include #include class Git; class FileHistory; class QString; class UnbufferedTemporaryFile; // data exchange facility with 'git log' could be based on QProcess or on // a temporary file (default). Uncomment following line to use QProcess // #define USE_QPROCESS class DataLoader : public QProcess { Q_OBJECT public: DataLoader(Git* g, FileHistory* f); ~DataLoader(); bool start(const QStringList& args, const QString& wd, const QString& buf); signals: void newDataReady(const FileHistory*); void loaded(FileHistory*,ulong,int,bool,const QString&,const QString&); private slots: void on_finished(int, QProcess::ExitStatus); void on_cancel(); void on_cancel(const FileHistory*); void on_timeout(); private: void parseSingleBuffer(const QByteArray& ba); void baAppend(QByteArray** src, const char* ascii, int len); void addSplittedChunks(const QByteArray* halfChunk); bool createTemporaryFile(); ulong readNewData(bool lastBuffer); Git* git; FileHistory* fh; QByteArray* halfChunk; UnbufferedTemporaryFile* dataFile; QTime loadTime; QTimer guiUpdateTimer; ulong loadedBytes; bool isProcExited; bool parsing; bool canceling; }; #endif qgit-2.7/src/domain.cpp000066400000000000000000000210511305655150700150760ustar00rootroot00000000000000/* Author: Marco Costalba (C) 2005-2007 Copyright: See COPYING file that comes with this distribution */ #include #include #include #include "FileHistory.h" #include "exceptionmanager.h" #include "mainimpl.h" #include "git.h" #include "domain.h" using namespace QGit; void StateInfo::S::clear() { sha = fn = dtSha = ""; isM = allM = false; sel = true; } bool StateInfo::S::operator==(const StateInfo::S& st) const { if (&st == this) return true; return ( sha == st.sha && fn == st.fn && dtSha == st.dtSha && sel == st.sel && isM == st.isM && allM == st.allM); } bool StateInfo::S::operator!=(const StateInfo::S& st) const { return !(StateInfo::S::operator==(st)); } void StateInfo::clear() { nextS.clear(); curS.clear(); prevS.clear(); isLocked = false; } StateInfo& StateInfo::operator=(const StateInfo& newState) { if (&newState != this) { if (isLocked) nextS = newState.curS; else curS = newState.curS; // prevS is mot modified to allow a rollback } return *this; } bool StateInfo::operator==(const StateInfo& newState) const { if (&newState == this) return true; return (curS == newState.curS); // compare is made on curS only } bool StateInfo::operator!=(const StateInfo& newState) const { return !(StateInfo::operator==(newState)); } bool StateInfo::isChanged(uint what) const { bool ret = false; if (what & SHA) ret = (sha(true) != sha(false)); if (!ret && (what & FILE_NAME)) ret = (fileName(true) != fileName(false)); if (!ret && (what & DIFF_TO_SHA)) ret = (diffToSha(true) != diffToSha(false)); if (!ret && (what & ALL_MERGE_FILES)) ret = (allMergeFiles(true) != allMergeFiles(false)); return ret; } // ************************* Domain **************************** Domain::Domain(MainImpl* m, Git* g, bool isMain) : QObject(m), git(g) { EM_INIT(exDeleteRequest, "Deleting domain"); EM_INIT(exCancelRequest, "Canceling update"); fileHistory = new FileHistory(this, git); if (isMain) git->setDefaultModel(fileHistory); st.clear(); busy = readyToDrag = dragging = dropping = linked = false; popupType = 0; container = new QWidget(NULL); // will be reparented to m()->tabWdg } Domain::~Domain() { if (!parent()) return; // remove before to delete, avoids a Qt warning in QInputContext() m()->tabWdg->removeTab(m()->tabWdg->indexOf(container)); container->deleteLater(); } void Domain::clear(bool complete) { if (complete) st.clear(); fileHistory->clear(); } void Domain::on_closeAllTabs() { delete this; // must be sync, deleteLater() does not work } void Domain::deleteWhenDone() { if (!EM_IS_PENDING(exDeleteRequest)) EM_RAISE(exDeleteRequest); emit cancelDomainProcesses(); on_deleteWhenDone(); } void Domain::on_deleteWhenDone() { if (!EM_IS_PENDING(exDeleteRequest)) deleteLater(); else QTimer::singleShot(20, this, SLOT(on_deleteWhenDone())); } void Domain::setThrowOnDelete(bool b) { if (b) EM_REGISTER(exDeleteRequest); else EM_REMOVE(exDeleteRequest); } bool Domain::isThrowOnDeleteRaised(int excpId, SCRef curContext) { return EM_MATCH(excpId, exDeleteRequest, curContext); } MainImpl* Domain::m() const { return static_cast(parent()); } void Domain::showStatusBarMessage(const QString& msg, int timeout) { m()->statusBar()->showMessage(msg, timeout); } void Domain::setTabCaption(const QString& caption) { int idx = m()->tabWdg->indexOf(container); m()->tabWdg->setTabText(idx, caption); } bool Domain::setReadyToDrag(bool b) { readyToDrag = (b && !busy && !dragging && !dropping); return readyToDrag; } bool Domain::setDragging(bool b) { bool dragFinished = (!b && dragging); dragging = (b && readyToDrag && !dropping); if (dragging) readyToDrag = false; if (dragFinished) flushQueue(); return dragging; } void Domain::unlinkDomain(Domain* d) { d->linked = false; while (d->disconnect(SIGNAL(updateRequested(StateInfo)), this)) ;// a signal is emitted for every connection you make, // so if you duplicate a connection, two signals will be emitted. } void Domain::linkDomain(Domain* d) { unlinkDomain(d); // be sure only one connection is active connect(d, SIGNAL(updateRequested(StateInfo)), this, SLOT(on_updateRequested(StateInfo))); d->linked = true; } void Domain::on_updateRequested(StateInfo newSt) { st = newSt; UPDATE(); } bool Domain::flushQueue() { // during dragging any state update is queued, so try to flush pending now if (!busy && st.flushQueue()) { UPDATE(); return true; } return false; } bool Domain::event(QEvent* e) { bool fromMaster = false; switch (int(e->type())) { case UPD_DM_MST_EV: fromMaster = true; // fall through case UPD_DM_EV: update(fromMaster, ((UpdateDomainEvent*)e)->isForced()); break; case MSG_EV: if (!busy && !st.requestPending()) QApplication::postEvent(m(), new MessageEvent(((MessageEvent*)e)->myData())); else // waiting for the end of updating statusBarRequest = ((MessageEvent*)e)->myData(); break; default: break; } return QObject::event(e); } void Domain::populateState() { const Rev* r = git->revLookup(st.sha()); if (r) st.setIsMerge(r->parentsCount() > 1); } void Domain::update(bool fromMaster, bool force) { if (busy && st.requestPending()) { // quick exit current (obsoleted) update but only if state // is going to change. Without this check calling update() // many times with the same data nullify the update EM_RAISE(exCancelRequest); emit cancelDomainProcesses(); } if (busy || dragging) return; if (linked && !fromMaster) { // in this case let the update to fall down from master domain StateInfo tmp(st); st.rollBack(); // we don't want to filter out next update sent from master emit updateRequested(tmp); return; } try { EM_REGISTER_Q(exCancelRequest); // quiet, no messages when thrown setThrowOnDelete(true); git->setThrowOnStop(true); git->setCurContext(this); busy = true; setReadyToDrag(false); // do not start any drag while updating populateState(); // complete any missing state information st.setLock(true); // any state change will be queued now if (doUpdate(force)) st.commit(); else st.rollBack(); st.setLock(false); busy = false; if (git->curContext() != this) qDebug("ASSERT in Domain::update, context is %p " "instead of %p", (void*)git->curContext(), (void*)this); git->setCurContext(NULL); git->setThrowOnStop(false); setThrowOnDelete(false); EM_REMOVE(exCancelRequest); } catch (int i) { /* If we have a cancel request because of a new update is in queue we cannot roolback current state to avoid new update is filtered out in case rolled back sha and new sha are the same. This could happen with arrow navigation: sha -> go UP (new sha) -> go DOWN (cancel) -> rollback to sha And pending request 'sha' is filtered out leaving us in an inconsistent state. */ st.commit(); st.setLock(false); busy = false; git->setCurContext(NULL); git->setThrowOnStop(false); setThrowOnDelete(false); EM_REMOVE(exCancelRequest); if (QApplication::overrideCursor()) QApplication::restoreOverrideCursor(); QString context("updating "); if (git->isThrowOnStopRaised(i, context + metaObject()->className())) { EM_THROW_PENDING; return; } if (isThrowOnDeleteRaised(i, context + metaObject()->className())) { EM_THROW_PENDING; return; } if (i == exCancelRequest) EM_THROW_PENDING; // do not return else { const QString info("Exception \'" + EM_DESC(i) + "\' " "not handled in init...re-throw"); dbs(info); throw; } } bool nextRequestPending = flushQueue(); if (!nextRequestPending && !statusBarRequest.isEmpty()) { // update status bar when we are sure no more work is pending QApplication::postEvent(m(), new MessageEvent(statusBarRequest)); statusBarRequest = ""; } if (!nextRequestPending && popupType) sendPopupEvent(); } void Domain::sendPopupEvent() { // call an async context popup, must be executed // after returning to event loop DeferredPopupEvent* e = new DeferredPopupEvent(popupData, popupType); QApplication::postEvent(m(), e); popupType = 0; } void Domain::on_contextMenu(const QString& data, int type) { popupType = type; popupData = data; if (busy) return; // we are in the middle of an update // if list view is already updated pop-up // context menu, otherwise it means update() // has still not been called, a pop-up request, // will be fired up at the end of next update() if ((type == POPUP_LIST_EV) && (data != st.sha())) return; sendPopupEvent(); } qgit-2.7/src/domain.h000066400000000000000000000113451305655150700145500ustar00rootroot00000000000000/* Author: Marco Costalba (C) 2005-2007 Copyright: See COPYING file that comes with this distribution */ #ifndef DOMAIN_H #define DOMAIN_H #include #include #include "exceptionmanager.h" #include "common.h" #define UPDATE_DOMAIN(x) QApplication::postEvent(x, new UpdateDomainEvent(false)) #define UPDATE() QApplication::postEvent(this, new UpdateDomainEvent(false)) #define UPDATE_DM_MASTER(x, f) QApplication::postEvent(x, new UpdateDomainEvent(true, f)) class Domain; class FileHistory; class Git; class MainImpl; class UpdateDomainEvent : public QEvent { public: explicit UpdateDomainEvent(bool fromMaster, bool force = false) : QEvent(fromMaster ? (QEvent::Type)QGit::UPD_DM_MST_EV : (QEvent::Type)QGit::UPD_DM_EV), f(force) {} bool isForced() const { return f; } private: bool f; }; class StateInfo { public: StateInfo() { clear(); } StateInfo& operator=(const StateInfo& newState); bool operator==(const StateInfo& newState) const; bool operator!=(const StateInfo& newState) const; void clear(); const QString sha(bool n = true) const { return (n ? curS.sha : prevS.sha); } const QString fileName(bool n = true) const { return (n ? curS.fn : prevS.fn); } const QString diffToSha(bool n = true) const {return(n ? curS.dtSha : prevS.dtSha); } bool selectItem(bool n = true) const { return (n ? curS.sel : prevS.sel); } bool isMerge(bool n = true) const { return (n ? curS.isM : prevS.isM); } bool allMergeFiles(bool n = true) const { return (n ? curS.allM : prevS.allM); } void setSha(const QString& s) { if (isLocked) nextS.sha = s; else curS.sha = s; } void setFileName(const QString& s) { if (isLocked) nextS.fn = s; else curS.fn = s; } void setDiffToSha(const QString& s) { if (isLocked) nextS.dtSha = s; else curS.dtSha = s; } void setSelectItem(bool b) { if (isLocked) nextS.sel = b; else curS.sel = b; } void setIsMerge(bool b) { if (isLocked) nextS.isM = b; else curS.isM = b; } void setAllMergeFiles(bool b) { if (isLocked) nextS.allM = b; else curS.allM = b; } bool isChanged(uint what = ANY) const; enum Field { SHA = 1, FILE_NAME = 2, DIFF_TO_SHA = 4, ALL_MERGE_FILES = 8, ANY = 15 }; private: friend class Domain; bool requestPending() const { return (!nextS.sha.isEmpty() && (nextS != curS)); } void setLock(bool b) { isLocked = b; if (b) nextS = curS; } void commit() { prevS = curS; } void rollBack() { if (nextS == curS) nextS.clear(); // invalidate to avoid infinite loop curS = prevS; } bool flushQueue() { if (requestPending()) { curS = nextS; return true; } return false; } class S { public: S() { clear(); } void clear(); bool operator==(const S& newState) const; bool operator!=(const S& newState) const; QString sha; QString fn; QString dtSha; bool sel; bool isM; bool allM; }; S curS; // current state, what returns from StateInfo::sha() S prevS; // previous good state, used to rollBack in case state update fails S nextS; // next queued state, waiting for current update to finish bool isLocked; }; class Domain: public QObject { Q_OBJECT public: Domain() {} Domain(MainImpl* m, Git* git, bool isMain); virtual ~Domain(); void deleteWhenDone(); // will delete when no more run() are pending void showStatusBarMessage(const QString& msg, int timeout = 0); void setThrowOnDelete(bool b); bool isThrowOnDeleteRaised(int excpId, SCRef curContext); MainImpl* m() const; FileHistory* model() const { return fileHistory; } bool isReadyToDrag() const { return readyToDrag; } bool setReadyToDrag(bool b); bool isDragging() const { return dragging; } bool setDragging(bool b); bool isDropping() const { return dropping; } void setDropping(bool b) { dropping = b; } bool isLinked() const { return linked; } QWidget* tabPage() const { return container; } virtual bool isMatch(SCRef) { return false; } StateInfo st; signals: void updateRequested(StateInfo newSt); void cancelDomainProcesses(); public slots: void on_closeAllTabs(); protected slots: virtual void on_contextMenu(const QString&, int); void on_updateRequested(StateInfo newSt); void on_deleteWhenDone(); protected: virtual void clear(bool complete = true); virtual bool event(QEvent* e); virtual bool doUpdate(bool force) = 0; void linkDomain(Domain* d); void unlinkDomain(Domain* d); void setTabCaption(const QString& caption); Git* git; QWidget* container; bool busy; private: void populateState(); void update(bool fromMaster, bool force); bool flushQueue(); void sendPopupEvent(); EM_DECLARE(exDeleteRequest); EM_DECLARE(exCancelRequest); FileHistory* fileHistory; bool readyToDrag; bool dragging; bool dropping; bool linked; int popupType; QString popupData; QString statusBarRequest; }; #endif qgit-2.7/src/exceptionmanager.cpp000066400000000000000000000114571305655150700171710ustar00rootroot00000000000000/* Description: Support for recursive C++ exception handling compatible with Qt qApp->processEvents() See exception_manager.txt for details. Author: Marco Costalba (C) 2005-2007 Copyright: See COPYING file that comes with this distribution */ #include #include "exceptionmanager.h" ExceptionManager::ExceptionManager() { excpId = regionId = currentRegionId = 1; descriptions.append("we start from 1"); } void ExceptionManager::init(int* excp, const QString& desc) { *excp = excpId++; descriptions.append(desc); } const QString ExceptionManager::desc(int excpId) { return descriptions[excpId]; } bool ExceptionManager::isMatch(int value, int excp, const QString& context) { bool match = (value == excp); if (match) { QString info("Caught exception \'" + descriptions[excp] + "\' while in " + context); qDebug("%s", info.toLatin1().constData()); } return match; } void ExceptionManager::add(int excpId, bool verbose) { // add a new exception in currentThrowableSet // are prepended so to use a for loop starting // from begin to find the latest. Exceptions are // always added/removed from both totalThrowableSet // and regionThrowableSet totalThrowableSet.prepend(Exception(excpId, verbose)); regionThrowableSet.prepend(Exception(excpId, verbose)); } void ExceptionManager::remove(int excpId) { // removes ONE exception in totalThrowableSet and ONE in regionThrowableSet. // if add and remove calls are correctly nested the removed // excp should be the first in both throwable sets if (totalThrowableSet.isEmpty() || regionThrowableSet.isEmpty()) { qDebug("ASSERT in remove: removing %i from an empty set", excpId); return; } // remove from region. SetIt itReg(regionThrowableSet.begin()); if ((*itReg).excpId != excpId) { qDebug("ASSERT in remove: %i is not the first in list", excpId); return; } regionThrowableSet.erase(itReg); // remove from total. SetIt itTot(totalThrowableSet.begin()); if ((*itTot).excpId != excpId) { qDebug("ASSERT in remove: %i is not the first in list", excpId); return; } totalThrowableSet.erase(itTot); } ExceptionManager::SetIt ExceptionManager::findExcp(ThrowableSet& ts, const SetIt& startIt, int excpId) { SetIt it(startIt); for ( ; it != ts.end(); ++it) if ((*it).excpId == excpId) break; return it; } void ExceptionManager::setRaisedFlag(ThrowableSet& ts, int excpId) { SetIt it(findExcp(ts, ts.begin(), excpId)); while (it != ts.end()) { (*it).isRaised = true; it = findExcp(ts, ++it, excpId); } } void ExceptionManager::raise(int excpId) { if (totalThrowableSet.isEmpty()) return; // check totalThrowableSet to find if excpId is throwable SetIt it = findExcp(totalThrowableSet, totalThrowableSet.begin(), excpId); if (it == totalThrowableSet.end()) return; // we have found an exception. Set raised flag in regionThrowableSet setRaisedFlag(regionThrowableSet, excpId); // then set the flag in all regions throwableSetList QMap::iterator itList(throwableSetMap.begin()); while (itList != throwableSetMap.end()) { setRaisedFlag(*itList, excpId); ++itList; } } int ExceptionManager::saveThrowableSet() { // here we save regionThrowableSet _and_ update the region. // regionThrowableSet is saved with the current region index. // then current region is changed to a new and never used index int oldCurrentRegionId = currentRegionId; throwableSetMap.insert(currentRegionId, regionThrowableSet); currentRegionId = ++regionId; // we use this call to trigger a region boundary crossing // so we have to clear the new region throwables. We still // have totalThrowableSet to catch any request. regionThrowableSet.clear(); return oldCurrentRegionId; } void ExceptionManager::restoreThrowableSet(int regionId) { if (!throwableSetMap.contains(regionId)) { qDebug("ASSERT in restoreThrowableSet: region %i not found", regionId); return; } regionThrowableSet = throwableSetMap[regionId]; throwableSetMap.remove(regionId); } bool ExceptionManager::isPending(int excpId) { // check in ALL regions if an exception request is pending QMap::const_iterator itList(throwableSetMap.constBegin()); while (itList != throwableSetMap.constEnd()) { ThrowableSet::const_iterator it((*itList).constBegin()); for ( ; it != (*itList).constEnd(); ++it) if ((*it).isRaised && (*it).excpId == excpId) return true; ++itList; } return false; } void ExceptionManager::throwPending() { if (regionThrowableSet.isEmpty()) return; ThrowableSet::const_iterator it(regionThrowableSet.constBegin()); for ( ; it != regionThrowableSet.constEnd(); ++it) if ((*it).isRaised) break; if (it == regionThrowableSet.constEnd()) return; int excpToThrow = (*it).excpId; if ((*it).verbose) qDebug("Thrown exception \'%s\'", desc(excpToThrow).toLatin1().constData()); throw excpToThrow; } qgit-2.7/src/exceptionmanager.h000066400000000000000000000055001305655150700166260ustar00rootroot00000000000000/* Author: Marco Costalba (C) 2005-2007 Copyright: See COPYING file that comes with this distribution */ #ifndef EXCEPTIONMANAGER_H #define EXCEPTIONMANAGER_H #include #include #include // exception manager sugar macro #define EM_DECLARE(x) int x #define EM_INIT(x,y) ExceptionManager::em()->init(&x, QString(y)) #define EM_REGISTER(x) ExceptionManager::em()->add(x, true) #define EM_REGISTER_Q(x) ExceptionManager::em()->add(x, false) #define EM_REMOVE(x) ExceptionManager::em()->remove(x) #define EM_RAISE(x) ExceptionManager::em()->raise(x) #define EM_MATCH(x,y,z) ExceptionManager::em()->isMatch(x,y,QString(z)) #define EM_DESC(x) ExceptionManager::em()->desc(x) #define EM_IS_PENDING(x) ExceptionManager::em()->isPending(x) #define EM_THROW_PENDING ExceptionManager::em()->throwPending() #define EM_BEFORE_PROCESS_EVENTS int _region = ExceptionManager::em()->saveThrowableSet() #define EM_AFTER_PROCESS_EVENTS ExceptionManager::em()->restoreThrowableSet(_region); \ ExceptionManager::em()->throwPending() #define EM_PROCESS_EVENTS do { EM_BEFORE_PROCESS_EVENTS; \ qApp->processEvents(); \ EM_AFTER_PROCESS_EVENTS; \ } while (false) #define EM_PROCESS_EVENTS_NO_INPUT do { EM_BEFORE_PROCESS_EVENTS; \ qApp->processEvents(QEventLoop::ExcludeUserInputEvents); \ EM_AFTER_PROCESS_EVENTS; \ } while (false) class ExceptionManager { // singleton class protected: ExceptionManager(); ExceptionManager(const ExceptionManager&); ExceptionManager& operator=(const ExceptionManager&); public: static ExceptionManager* em() { static ExceptionManager private_em; return &private_em; } void init(int* excpId, const QString& desc); void add(int excpId, bool verbose); void remove(int excpId); void raise(int excpId); void throwPending(); int saveThrowableSet(); void restoreThrowableSet(int regionId); bool isMatch(int value, int excpId, const QString& context); const QString desc(int excpId); bool isPending(int excpId); private: int excpId; int regionId; int currentRegionId; class Exception { public: Exception() {} Exception(int ex, bool v) : excpId(ex), verbose(v), isRaised(false) {} int excpId; bool verbose; bool isRaised; }; typedef QList ThrowableSet; typedef ThrowableSet::iterator SetIt; QMap throwableSetMap; ThrowableSet totalThrowableSet; ThrowableSet regionThrowableSet; QVector descriptions; SetIt findExcp(ThrowableSet& ts, const SetIt& startIt, int excpId); void setRaisedFlag(ThrowableSet& ts, int excpId); }; #endif qgit-2.7/src/filecontent.cpp000066400000000000000000000375551305655150700161610ustar00rootroot00000000000000/* Author: Marco Costalba (C) 2005-2007 Copyright: See COPYING file that comes with this distribution */ #include #include #include #include #include #include #include #include #include #include #include "domain.h" #include "myprocess.h" #include "mainimpl.h" #include "git.h" #include "annotate.h" #include "filecontent.h" class FileHighlighter : public QSyntaxHighlighter { public: FileHighlighter(FileContent* fc) : QSyntaxHighlighter(fc), f(fc) {} virtual void highlightBlock(const QString& p) { // state is used to count lines, starting from 0 if (currentBlockState() == -1) // only once setCurrentBlockState(previousBlockState() + 1); if (f->isHtmlSource) return; if ( f->isRangeFilterActive && f->rangeInfo->start != 0 && f->rangeInfo->start <= currentBlockState() && f->rangeInfo->end >= currentBlockState()) { QTextCharFormat myFormat; myFormat.setFontWeight(QFont::Bold); myFormat.setForeground(Qt::blue); setFormat(0, p.length(), myFormat); } return; } private: FileContent* f; }; FileContent::FileContent(QWidget* parent) : QTextEdit(parent) { isRangeFilterActive = isHtmlSource = isImageFile = isAnnotationAppended = false; isShowAnnotate = true; rangeInfo = new RangeInfo(); fileHighlighter = new FileHighlighter(this); setFont(QGit::TYPE_WRITER_FONT); } FileContent::~FileContent() { clearAll(!optEmitSignal); delete fileHighlighter; delete rangeInfo; } void FileContent::setup(Domain* dm, Git* g, QListWidget* lw) { d = dm; git = g; st = &(d->st); listWidgetAnn = lw; lw->setParent(this); lw->setSelectionMode(QAbstractItemView::NoSelection); QPalette pl = lw->palette(); pl.setColor(QPalette::Text, Qt::lightGray); lw->setPalette(pl); clearAll(!optEmitSignal); connect(d->m(), SIGNAL(typeWriterFontChanged()), this, SLOT(typeWriterFontChanged())); connect(git, SIGNAL(annotateReady(Annotate*, bool, const QString&)), this, SLOT(on_annotateReady(Annotate*, bool, const QString&))); connect(listWidgetAnn, SIGNAL(itemDoubleClicked(QListWidgetItem*)), this, SLOT(on_list_doubleClicked(QListWidgetItem*))); QScrollBar* vsb = verticalScrollBar(); connect(vsb, SIGNAL(valueChanged(int)), this, SLOT(on_scrollBar_valueChanged(int))); vsb->setSingleStep(fontMetrics().lineSpacing()); vsb = listWidgetAnn->verticalScrollBar(); connect(vsb, SIGNAL(valueChanged(int)), this, SLOT(on_listScrollBar_valueChanged(int))); vsb->setSingleStep(fontMetrics().lineSpacing()); } void FileContent::on_scrollBar_valueChanged(int value) { listWidgetAnn->verticalScrollBar()->setValue(value); } void FileContent::on_listScrollBar_valueChanged(int value) { verticalScrollBar()->setValue(value); } int FileContent::itemAnnId(QListWidgetItem* item) { if (item == NULL) return 0; QString id(item->text()); if (!id.contains('.')) return 0; return id.section('.', 0, 0).toInt(); } void FileContent::on_list_doubleClicked(QListWidgetItem* item) { int id = itemAnnId(item); if (id) emit revIdSelected(id); } void FileContent::clearAnnotate(bool emitSignal) { git->cancelAnnotate(annotateObj); annotateObj = NULL; curAnn = NULL; isAnnotationLoading = false; if (emitSignal) emit annotationAvailable(false); } void FileContent::clearText(bool emitSignal) { git->cancelProcess(proc); proc = NULL; fileRowData.clear(); QTextEdit::clear(); // explicit call because our clear() is only declared listWidgetAnn->clear(); isFileAvail = isAnnotationAppended = false; if (emitSignal) emit fileAvailable(false); } void FileContent::clearAll(bool emitSignal) { clearAnnotate(emitSignal); clearText(emitSignal); } void FileContent::setShowAnnotate(bool b) { // add an annotation if is available and still not appended, this // can happen if annotation became available while loading the file. // If isShowAnnotate is false try to remove any annotation. isShowAnnotate = b; if ( !isFileAvail || (!curAnn && isShowAnnotate) || (isAnnotationAppended == isShowAnnotate)) return; setAnnList(); } void FileContent::setHighlightSource(bool b) { if (b && !git->isTextHighlighter()) { dbs("ASSERT in setHighlightSource: no highlighter found"); return; } isHtmlSource = b; doUpdate(true); } void FileContent::doUpdate(bool force) { bool shaChanged = (st->sha(true) != st->sha(false)); bool fileNameChanged = (st->fileName(true) != st->fileName(false)); if (!fileNameChanged && !shaChanged && !force) return; saveScreenState(); if (fileNameChanged) { clearAll(optEmitSignal); isImageFile = Git::isImageFile(st->fileName()); } else clearText(optEmitSignal); lookupAnnotation(); // before file loading QString fileSha; if (curAnn) fileSha = curAnn->fileSha; else fileSha = git->getFileSha(st->fileName(), st->sha()); // both calls bound procFinished() and procReadyRead() slots if (isHtmlSource && !isImageFile) proc = git->getHighlightedFile(fileSha, this, NULL, st->fileName()); else proc = git->getFile(fileSha, this, NULL, st->fileName()); // non blocking ss.isValid = false; if (isRangeFilterActive) getRange(st->sha(), rangeInfo); else if (curAnn) { // call seekPosition() while loading the file so to shadow the compute time int& from = ss.hasSelectedText ? ss.paraFrom : ss.topPara; int& to = ss.hasSelectedText ? ss.paraTo : ss.topPara; ss.isValid = annotateObj->seekPosition(&from, &to, st->sha(false), st->sha(true)); } } bool FileContent::startAnnotate(FileHistory* fh, SCRef ht) { if (!isImageFile) annotateObj = git->startAnnotate(fh, d); // non blocking histTime = ht; isAnnotationLoading = (annotateObj != NULL); return isAnnotationLoading; } uint FileContent::annotateLength(const FileAnnotation* annFile) { int maxLen = 0; FOREACH_SL (it, annFile->lines) if ((*it).length() > maxLen) maxLen = (*it).length(); return maxLen; } bool FileContent::getRange(SCRef sha, RangeInfo* r) { if (annotateObj) return annotateObj->getRange(sha, r); return false; } void FileContent::scrollCursorToTop() { QScrollBar* vsb = verticalScrollBar(); vsb->setValue(vsb->value() + cursorRect().top()); } void FileContent::scrollLineToTop(int lineNum) { QTextCursor tc = textCursor(); tc.movePosition(QTextCursor::Start); tc.movePosition(QTextCursor::NextBlock, QTextCursor::MoveAnchor, lineNum); setTextCursor(tc); scrollCursorToTop(); } int FileContent::positionToLineNum(int pos) { QTextCursor tc = textCursor(); if (pos != -1) tc.setPosition(pos); return tc.blockNumber(); } int FileContent::lineAtTop() { return cursorForPosition(QPoint(1, 1)).blockNumber(); } void FileContent::setSelection(int paraFrom, int indexFrom, int paraTo, int indexTo) { scrollLineToTop(paraFrom); QTextCursor tc = textCursor(); tc.setPosition(tc.position() + indexFrom); tc.movePosition(QTextCursor::StartOfBlock, QTextCursor::KeepAnchor); int delta = paraTo - paraFrom; tc.movePosition(QTextCursor::NextBlock, QTextCursor::KeepAnchor, delta); tc.movePosition(QTextCursor::Right, QTextCursor::KeepAnchor, indexTo); setTextCursor(tc); } void FileContent::saveScreenState() { ss.isValid = true; QTextCursor tc = textCursor(); ss.hasSelectedText = tc.hasSelection(); if (ss.hasSelectedText) { int endPos = tc.selectionEnd(); ss.paraFrom = positionToLineNum(tc.selectionStart()); ss.paraTo = positionToLineNum(tc.selectionEnd()); tc.setPosition(tc.selectionStart()); ss.indexFrom = tc.columnNumber(); tc.setPosition(endPos); ss.indexTo = tc.columnNumber(); } else ss.topPara = lineAtTop(); } void FileContent::restoreScreenState() { if (!ss.isValid) return; if (ss.hasSelectedText) setSelection(ss.paraFrom, ss.indexFrom, ss.paraTo, ss.indexTo); else scrollLineToTop(ss.topPara); } void FileContent::goToAnnotation(int revId, int dir) { if (!isAnnotationAppended || !curAnn || (revId == 0)) return; const QString header(QString::number(revId) + "."); int row = (dir == 0 ? -1 : lineAtTop()); QListWidgetItem* itm = NULL; do { row += (dir >= 0 ? 1 : -1); itm = listWidgetAnn->item(row); if (itm && itm->text().trimmed().startsWith(header)) { scrollLineToTop(row); break; } } while (itm); } bool FileContent::goToRangeStart() { if ( !isRangeFilterActive || !curAnn || (rangeInfo->start == 0)) return false; scrollLineToTop(rangeInfo->start); return true; } void FileContent::copySelection() { QClipboard* cb = QApplication::clipboard(); QString sel(textCursor().selectedText()); /* Workaround a Qt issue, QTextCursor::selectedText() substitutes '\n' with '\0'. Restore proper content. QString::replace() doesn't seem to work, go with a loop. */ for (int i = 0; i < sel.length(); i++) { const char c = sel.at(i).toLatin1(); if (c == 0) sel[i] = '\n'; } cb->setText(sel, QClipboard::Clipboard); } bool FileContent::rangeFilter(bool b) { isRangeFilterActive = false; if (b) { if (!annotateObj) { dbs("ASSERT in rangeFilter: annotateObj not available"); return false; } QApplication::setOverrideCursor(QCursor(Qt::WaitCursor)); EM_PROCESS_EVENTS_NO_INPUT; QString ancestor; QTextCursor tc = textCursor(); int paraFrom = positionToLineNum(tc.selectionStart()); int paraTo = positionToLineNum(tc.selectionEnd()); try { d->setThrowOnDelete(true); // could call qApp->processEvents() ancestor = annotateObj->computeRanges(st->sha(), paraFrom, paraTo); d->setThrowOnDelete(false); } catch (int i) { d->setThrowOnDelete(false); QApplication::restoreOverrideCursor(); if (d->isThrowOnDeleteRaised(i, "range filtering")) { EM_THROW_PENDING; return false; } const QString info("Exception \'" + EM_DESC(i) + "\' " "not handled in lookupAnnotation...re-throw"); dbs(info); throw; } QApplication::restoreOverrideCursor(); if (!ancestor.isEmpty() && getRange(ancestor, rangeInfo)) { isRangeFilterActive = true; fileHighlighter->rehighlight(); goToRangeStart(); return true; } } else { setFontWeight(QFont::Normal); // bold if the first line was highlighted fileHighlighter->rehighlight(); setSelection(rangeInfo->start, 0, rangeInfo->end, 0); rangeInfo->clear(); } return false; } bool FileContent::lookupAnnotation() { if ( st->sha().isEmpty() || st->fileName().isEmpty() || isAnnotationLoading || !annotateObj) return false; try { d->setThrowOnDelete(true); // could call qApp->processEvents() curAnn = git->lookupAnnotation(annotateObj, st->sha()); if (!curAnn) { dbp("ASSERT in lookupAnnotation: no annotation for %1", st->fileName()); clearAnnotate(optEmitSignal); } else if (curAnn->lines.empty()) curAnn = NULL; d->setThrowOnDelete(false); } catch (int i) { d->setThrowOnDelete(false); if (d->isThrowOnDeleteRaised(i, "looking up annotation")) { EM_THROW_PENDING; return false; } const QString info("Exception \'" + EM_DESC(i) + "\' " "not handled in lookupAnnotation...re-throw"); dbs(info); throw; } return (curAnn != NULL); } void FileContent::on_annotateReady(Annotate* readyAnn, bool ok, const QString& msg) { if (readyAnn != annotateObj) // Git::annotateReady() is sent to all receivers return; isAnnotationLoading = false; if (!ok) { d->showStatusBarMessage("Sorry, annotation not available for this file."); return; } QString fileNum = msg.section(' ', 0, 0); QString annTime = msg.section(' ', 1, 1); QString stats("File '%1': revisions %2, history loaded in %3 ms, files annotated in %4 ms"); d->showStatusBarMessage(stats.arg(st->fileName(), fileNum, histTime, annTime), 12000); if (lookupAnnotation()) emit annotationAvailable(true); } void FileContent::typeWriterFontChanged() { setFont(QGit::TYPE_WRITER_FONT); if (!isHtmlSource && !isImageFile && isFileAvail) { setPlainText(toPlainText()); setAnnList(); } } void FileContent::procReadyRead(const QByteArray& fileChunk) { fileRowData.append(fileChunk); // set text at the end of loading, much faster } void FileContent::procFinished(bool emitSignal) { if (isImageFile) showFileImage(); else { if (!fileRowData.endsWith("\n")) fileRowData.append('\n'); // fake a trailing new line if (isHtmlSource) setHtml(fileRowData); else { QTextCharFormat cf; // to restore also default color cf.setFont(font()); setCurrentCharFormat(cf); setPlainText(fileRowData); // much faster then append() } } setAnnList(); isFileAvail = true; if (ss.isValid) restoreScreenState(); // could be slow for big files else moveCursor(QTextCursor::Start); if (emitSignal) emit fileAvailable(true); } void FileContent::showFileImage() { QTemporaryFile f; if (f.open()) { QString header("

"); QGit::writeToFile(f.fileName(), fileRowData); setHtml(header); } } void FileContent::setAnnList() { int linesNum = document()->blockCount(); int linesNumDigits = QString::number(linesNum).length(); int curId = 0, annoMaxLen = 0; QStringList::const_iterator it, endIt; isAnnotationAppended = isShowAnnotate && curAnn; if (isAnnotationAppended) { annoMaxLen = annotateLength(curAnn); it = curAnn->lines.constBegin(); endIt = curAnn->lines.constEnd(); curId = curAnn->annId; } listWidgetAnn->setFont(currentFont()); QString tmp; tmp.fill('M', annoMaxLen + 1 + linesNumDigits + 2); int width = listWidgetAnn->fontMetrics().boundingRect(tmp).width(); QStringList sl; QVector curIdLines; for (int i = 0; i < linesNum; i++) { if (isAnnotationAppended) { if (it != endIt) tmp = (*(it++)).leftJustified(annoMaxLen); else tmp = QString().leftJustified(annoMaxLen); if (tmp.section('.',0 ,0).toInt() == curId) curIdLines.append(i); } else tmp.clear(); tmp.append(QString(" %1 ").arg(i + 1, linesNumDigits)); sl.append(tmp); } sl.append(QString()); // QTextEdit adds a blank line after content listWidgetAnn->setUpdatesEnabled(false); listWidgetAnn->clear(); listWidgetAnn->addItems(sl); QAbstractTextDocumentLayout *layout = document()->documentLayout(); if (layout != NULL) { qreal previousBottom = 0.; QTextBlock block = document()->begin(); for (int i = 0; i < linesNum; i++) { qreal bottom = layout->blockBoundingRect(block).bottom(); QListWidgetItem* item = listWidgetAnn->item(i); item->setSizeHint(QSize(0, static_cast(bottom - previousBottom))); item->setTextAlignment(Qt::AlignVCenter); // Move down a pixel or so. previousBottom = bottom; block = block.next(); } } QBrush fore(Qt::darkRed); QBrush back(Qt::lightGray); QFont f(listWidgetAnn->font()); f.setBold(true); FOREACH (QVector, it, curIdLines) { QListWidgetItem* item = listWidgetAnn->item(*it); item->setForeground(fore); item->setBackground(back); item->setFont(f); } /* When listWidgetAnn get focus for the first time the current item, if not already present, is set to the first row and scrolling starts from there, so set a proper current item here */ int topRow = lineAtTop() + 1; listWidgetAnn->setCurrentRow(topRow); listWidgetAnn->adjustSize(); // update scrollbar state adjustAnnListSize(width); listWidgetAnn->setUpdatesEnabled(true); } void FileContent::adjustAnnListSize(int width) { QRect r = listWidgetAnn->geometry(); r.setWidth(width); int height = geometry().height(); if (horizontalScrollBar()->isVisible()) height -= horizontalScrollBar()->height(); r.setHeight(height); listWidgetAnn->setGeometry(r); setViewportMargins(width, 0, 0, 0); // move textedit view to the left of listWidgetAnn } void FileContent::resizeEvent(QResizeEvent* e) { QTextEdit::resizeEvent(e); int width = listWidgetAnn->geometry().width(); adjustAnnListSize(width); // update list width } qgit-2.7/src/filecontent.h000066400000000000000000000055611305655150700156160ustar00rootroot00000000000000/* Author: Marco Costalba (C) 2005-2007 Copyright: See COPYING file that comes with this distribution */ #ifndef FILECONTENT_H #define FILECONTENT_H #include #include #include "common.h" class FileHighlighter; class Domain; class StateInfo; class Annotate; class Git; class MyProcess; class RangeInfo; class FileHistory; class QListWidget; class QListWidgetItem; class FileContent: public QTextEdit { Q_OBJECT public: FileContent(QWidget* parent); ~FileContent(); void setup(Domain* parent, Git* git, QListWidget* lwa); void doUpdate(bool force = false); void clearAll(bool emitSignal = true); void copySelection(); void goToAnnotation(int id, int direction); bool goToRangeStart(); bool rangeFilter(bool b); bool getRange(SCRef sha, RangeInfo* r); bool startAnnotate(FileHistory* fh, SCRef histTime); void setShowAnnotate(bool b); void setHighlightSource(bool b); void setSelection(int paraFrom, int indexFrom, int paraTo, int indexTo); int itemAnnId(QListWidgetItem* item); bool isFileAvailable() const { return isFileAvail; } bool isAnnotateAvailable() const { return curAnn != NULL; } signals: void annotationAvailable(bool); void fileAvailable(bool); void revIdSelected(int); public slots: void on_annotateReady(Annotate*, bool, const QString&); void procReadyRead(const QByteArray&); void procFinished(bool emitSignal = true); void typeWriterFontChanged(); protected: virtual void resizeEvent(QResizeEvent* e); private slots: void on_list_doubleClicked(QListWidgetItem*); void on_scrollBar_valueChanged(int); void on_listScrollBar_valueChanged(int); private: friend class FileHighlighter; void clear(); // declared as private, to avoid indirect access to QTextEdit::clear() void clearAnnotate(bool emitSignal); void clearText(bool emitSignal); void findInFile(SCRef str); void scrollCursorToTop(); void scrollLineToTop(int lineNum); int positionToLineNum(int pos = -1); int lineAtTop(); bool lookupAnnotation(); uint annotateLength(const FileAnnotation* curAnn); void saveScreenState(); void restoreScreenState(); void showFileImage(); void adjustAnnListSize(int width); void setAnnList(); Domain* d; Git* git; QListWidget* listWidgetAnn; StateInfo* st; RangeInfo* rangeInfo; FileHighlighter* fileHighlighter; QPointer proc; QPointer annotateObj; // valid from beginning of annotation loading const FileAnnotation* curAnn; // valid at the end of annotation loading QByteArray fileRowData; QString histTime; bool isFileAvail; bool isAnnotationLoading; bool isAnnotationAppended; bool isRangeFilterActive; bool isShowAnnotate; bool isHtmlSource; bool isImageFile; struct ScreenState { bool isValid, hasSelectedText; int topPara, paraFrom, indexFrom, paraTo, indexTo; }; ScreenState ss; enum BoolOption { // used as self-documenting boolean parameters optFalse, optEmitSignal }; }; #endif qgit-2.7/src/filelist.cpp000066400000000000000000000154651305655150700154560ustar00rootroot00000000000000/* Author: Marco Costalba (C) 2005-2007 Copyright: See COPYING file that comes with this distribution */ #include #include #include #include #include #include #include "git.h" #include "domain.h" #include "filelist.h" FileList::FileList(QWidget* p) : QListWidget(p), d(NULL), git(NULL), st(NULL) {} void FileList::setup(Domain* dm, Git* g) { d = dm; git = g; st = &(d->st); setFont(QGit::STD_FONT); connect(this, SIGNAL(customContextMenuRequested(const QPoint&)), this, SLOT(on_customContextMenuRequested(const QPoint&))); connect(this, SIGNAL(currentItemChanged(QListWidgetItem*, QListWidgetItem*)), this, SLOT(on_currentItemChanged(QListWidgetItem*, QListWidgetItem*))); } void FileList::addItem(const QString& label, const QColor& clr) { QListWidgetItem* item = new QListWidgetItem(label, this); item->setForeground(clr); } QString FileList::currentText() { QListWidgetItem* item = currentItem(); return (item ? item->data(Qt::DisplayRole).toString() : ""); } void FileList::on_changeFont(const QFont& f) { setFont(f); } void FileList::focusInEvent(QFocusEvent*) { // Workaround a Qt4.2 bug // // When an item is clicked and FileList still doesn't have the // focus we could have a double currentItemChanged() signal, // the first with current set to first item, the second with // current correctly set to the clicked one. // Oddly enough overriding this virtual function we remove // the spurious first signal if any. // // Unluckily in case the clicked one is the first in list we // have only one event and we could miss an update in that case, // so try to handle that if (!st->isMerge() && row(currentItem()) == 0) on_currentItemChanged(currentItem(), currentItem()); } void FileList::on_currentItemChanged(QListWidgetItem* current, QListWidgetItem*) { if (!current) return; if (st->isMerge() && row(current) == 0) { // header clicked // In a listbox without current item, as soon as the box // gains focus the first item becomes the current item // and a spurious currentChanged() signal is sent. // In case of a merge the signal arrives here and fakes // the user clicking on the header. // // The problem arise when user clicks on a merge header, // then list box gains focus and current item becomes null // because the content of the list is cleared and updated. // // If now tab is changed list box loose the focus and, // upon changing back again the tab the signal triggers // because Qt gives back the focus to the listbox. // // The workaround here is to give the focus away as soon // as the user clicks on the merge header. Note that a // lb->clearFocus() is not enough, we really need to // reassign the focus to someone else. d->tabPage()->setFocus(); st->setAllMergeFiles(!st->allMergeFiles()); } else { QString fileName(currentText()); git->removeExtraFileInfo(&fileName); // if we are called by updateFileList() fileName is already updated if (st->fileName() == fileName) // avoid loops return; st->setFileName(fileName); } st->setSelectItem(true); UPDATE_DOMAIN(d); } void FileList::on_customContextMenuRequested(const QPoint&) { int row = currentRow(); if (row == -1 || (row == 0 && st->isMerge())) // header clicked return; emit contextMenu(currentText(), QGit::POPUP_FILE_EV); } void FileList::mousePressEvent(QMouseEvent* e) { if (currentItem() && e->button() == Qt::LeftButton) { d->setReadyToDrag(true); dragFileName = currentText(); } QListWidget::mousePressEvent(e); } void FileList::mouseReleaseEvent(QMouseEvent* e) { d->setReadyToDrag(false); // in case of just click without moving QListWidget::mouseReleaseEvent(e); } void FileList::mouseMoveEvent(QMouseEvent* e) { if (d->isReadyToDrag()) { if (!d->setDragging(true)) return; if (dragFileName.isEmpty()) dbs("ASSERT in FileList::mouseMoveEvent() empty drag name"); QDrag* drag = new QDrag(this); QMimeData* mimeData = new QMimeData; mimeData->setText(dragFileName); drag->setMimeData(mimeData); dragFileName = ""; drag->start(); // blocking until drop event d->setDragging(false); } QListWidget::mouseMoveEvent(e); } void FileList::insertFiles(const RevFile* files) { clear(); if (!files) return; if (st->isMerge()) { const QString header((st->allMergeFiles()) ? "Click to view only interesting files" : "Click to view all merge files"); const bool useDark = QPalette().color(QPalette::Window).value() > QPalette().color(QPalette::WindowText).value(); QColor color (Qt::blue); if (!useDark) color = color.lighter(); addItem(header, color); } if (files->count() == 0) return; bool isMergeParents = !files->mergeParent.isEmpty(); int prevPar = (isMergeParents ? files->mergeParent.first() : 1); setUpdatesEnabled(false); for (int i = 0; i < files->count(); ++i) { if (files->statusCmp(i, RevFile::UNKNOWN)) continue; const bool useDark = QPalette().color(QPalette::Window).value() > QPalette().color(QPalette::WindowText).value(); QColor clr = palette().color(QPalette::WindowText); if (isMergeParents && files->mergeParent.at(i) != prevPar) { prevPar = files->mergeParent.at(i); new QListWidgetItem("", this); new QListWidgetItem("", this); } QString extSt(files->extendedStatus(i)); if (extSt.isEmpty()) { if (files->statusCmp(i, RevFile::NEW)) clr = useDark ? Qt::darkGreen : Qt::green; else if (files->statusCmp(i, RevFile::DELETED)) clr = Qt::red; } else { clr = useDark ? Qt::darkBlue : QColor(Qt::blue).lighter(); // in case of rename deleted file is not shown and... if (files->statusCmp(i, RevFile::DELETED)) continue; // ...new file is shown with extended info if (files->statusCmp(i, RevFile::NEW)) { addItem(extSt, clr); continue; } } addItem(git->filePath(*files, i), clr); } setUpdatesEnabled(true); } void FileList::update(const RevFile* files, bool newFiles) { QPalette pl = QApplication::palette(); if (!st->diffToSha().isEmpty()) pl.setColor(QPalette::Base, QGit::LIGHT_BLUE); setPalette(pl); if (newFiles) insertFiles(files); QString fileName(currentText()); git->removeExtraFileInfo(&fileName); // could be a renamed/copied file if (!fileName.isEmpty() && (fileName == st->fileName())) { currentItem()->setSelected(st->selectItem()); // just a refresh return; } clearSelection(); if (st->fileName().isEmpty()) return; QList l = findItems(st->fileName(), Qt::MatchExactly); if (l.isEmpty()) { // could be a renamed/copied file, try harder fileName = st->fileName(); git->addExtraFileInfo(&fileName, st->sha(), st->diffToSha(), st->allMergeFiles()); l = findItems(fileName, Qt::MatchExactly); } if (!l.isEmpty()) { setCurrentItem(l.first()); l.first()->setSelected(st->selectItem()); } } qgit-2.7/src/filelist.h000066400000000000000000000020061305655150700151060ustar00rootroot00000000000000/* Author: Marco Costalba (C) 2005-2007 Copyright: See COPYING file that comes with this distribution */ #ifndef FILELIST_H #define FILELIST_H #include #include "common.h" class Domain; class StateInfo; class Git; class FileList: public QListWidget { Q_OBJECT public: FileList(QWidget* parent); void setup(Domain* dm, Git* g); void update(const RevFile* files, bool newFiles); void addItem(const QString& label, const QColor& clr); QString currentText(); signals: void contextMenu(const QString&, int); public slots: void on_changeFont(const QFont& f); protected: virtual void focusInEvent(QFocusEvent*); virtual void mousePressEvent(QMouseEvent*); virtual void mouseMoveEvent(QMouseEvent*); virtual void mouseReleaseEvent(QMouseEvent*); private slots: void on_currentItemChanged(QListWidgetItem*, QListWidgetItem*); void on_customContextMenuRequested(const QPoint&); private: void insertFiles(const RevFile* files); Domain* d; Git* git; StateInfo* st; QString dragFileName; }; #endif qgit-2.7/src/fileview.cpp000066400000000000000000000252541305655150700154520ustar00rootroot00000000000000/* Description: file viewer window Author: Marco Costalba (C) 2005-2007 Copyright: See COPYING file that comes with this distribution */ #include #include "FileHistory.h" #include "mainimpl.h" #include "git.h" #include "annotate.h" #include "listview.h" #include "filecontent.h" #include "fileview.h" #define MAX_LINE_NUM 5 FileView::FileView(MainImpl* mi, Git* g) : Domain(mi, g, false) { fileTab = new Ui_TabFile(); fileTab->setupUi(container); fileTab->histListView->setup(this, git); fileTab->textEditFile->setup(this, git, fileTab->listWidgetAnn); // an empty string turn off the special-value text display fileTab->spinBoxRevision->setSpecialValueText(" "); // Add GNU source-highlight version to tooltip, or add a message that it's not installed. QToolButton* highlight = fileTab->toolButtonHighlightText; highlight->setToolTip(highlight->toolTip().arg(git->textHighlighterVersion())); clear(true); // init some stuff fileTab->listWidgetAnn->installEventFilter(this); connect(git, SIGNAL(loadCompleted(const FileHistory*, const QString&)), this, SLOT(on_loadCompleted(const FileHistory*, const QString&))); connect(m(), SIGNAL(changeFont(const QFont&)), fileTab->histListView, SLOT(on_changeFont(const QFont&))); connect(fileTab->histListView, SIGNAL(contextMenu(const QString&, int)), this, SLOT(on_contextMenu(const QString&, int))); connect(fileTab->textEditFile, SIGNAL(annotationAvailable(bool)), this, SLOT(on_annotationAvailable(bool))); connect(fileTab->textEditFile, SIGNAL(fileAvailable(bool)), this, SLOT(on_fileAvailable(bool))); connect(fileTab->textEditFile, SIGNAL(revIdSelected(int)), this, SLOT(on_revIdSelected(int))); connect(fileTab->toolButtonCopy, SIGNAL(clicked()), this, SLOT(on_toolButtonCopy_clicked())); connect(fileTab->toolButtonShowAnnotate, SIGNAL(toggled(bool)), this, SLOT(on_toolButtonShowAnnotate_toggled(bool))); connect(fileTab->toolButtonFindAnnotate, SIGNAL(toggled(bool)), this, SLOT(on_toolButtonFindAnnotate_toggled(bool))); connect(fileTab->toolButtonGoNext, SIGNAL(clicked()), this, SLOT(on_toolButtonGoNext_clicked())); connect(fileTab->toolButtonGoPrev, SIGNAL(clicked()), this, SLOT(on_toolButtonGoPrev_clicked())); connect(fileTab->toolButtonRangeFilter, SIGNAL(toggled(bool)), this, SLOT(on_toolButtonRangeFilter_toggled(bool))); connect(fileTab->toolButtonPin, SIGNAL(toggled(bool)), this, SLOT(on_toolButtonPin_toggled(bool))); connect(fileTab->toolButtonHighlightText, SIGNAL(toggled(bool)), this, SLOT(on_toolButtonHighlightText_toggled(bool))); connect(fileTab->spinBoxRevision, SIGNAL(valueChanged(int)), this, SLOT(on_spinBoxRevision_valueChanged(int))); } FileView::~FileView() { if (!parent()) return; delete fileTab->textEditFile; // must be deleted before fileTab delete fileTab; showStatusBarMessage(""); // cleanup any pending progress info QApplication::restoreOverrideCursor(); } bool FileView::eventFilter(QObject* obj, QEvent* e) { QListWidget* lw = fileTab->listWidgetAnn; if (e->type() == QEvent::ToolTip && obj == lw) { QHelpEvent* h = static_cast(e); int id = fileTab->textEditFile->itemAnnId(lw->itemAt(h->pos())); QRegExp re; SCRef sha(fileTab->histListView->shaFromAnnId(id)); SCRef d(git->getDesc(sha, re, re, false, model())); lw->setToolTip(d); } return QObject::eventFilter(obj, e); } void FileView::clear(bool complete) { Domain::clear(complete); if (complete) { setTabCaption("File"); fileTab->toolButtonCopy->setEnabled(false); } fileTab->textEditFile->clearAll(); // emits file/ann available signals fileTab->toolButtonPin->setEnabled(false); fileTab->toolButtonPin->setChecked(false); // TODO signals pressed() and clicked() are not emitted fileTab->spinBoxRevision->setEnabled(false); fileTab->spinBoxRevision->setValue(fileTab->spinBoxRevision->minimum()); // clears the box } bool FileView::goToCurrentAnnotation(int direction) { SCRef ids = fileTab->histListView->currentText(QGit::ANN_ID_COL); int id = (!ids.isEmpty() ? ids.toInt() : 0); fileTab->textEditFile->goToAnnotation(id, direction); return (id != 0); } void FileView::updateSpinBoxValue() { SCRef ids = fileTab->histListView->currentText(QGit::ANN_ID_COL); if ( ids.isEmpty() || !fileTab->spinBoxRevision->isEnabled() || fileTab->spinBoxRevision->value() == ids.toInt()) return; fileTab->spinBoxRevision->setValue(ids.toInt()); // emit QSpinBox::valueChanged() } bool FileView::isMatch(SCRef sha) { static RangeInfo r; // fast path here, avoid allocation on each call if (!fileTab->textEditFile->getRange(sha, &r)) return false; return r.modified; } void FileView::filterOnRange(bool isOn) { int matchedCnt = fileTab->histListView->filterRows(isOn, false); QString msg; if (isOn) msg = QString("Found %1 matches. Toggle filter " "button to remove the filter").arg(matchedCnt); showStatusBarMessage(msg); QApplication::postEvent(this, new MessageEvent(msg)); // deferred message, after update } bool FileView::doUpdate(bool force) { if (st.fileName().isEmpty()) return false; if (st.isChanged(StateInfo::FILE_NAME) || force) { clear(false); setTabCaption(st.fileName()); if (git->startFileHistory(st.sha(), st.fileName(), model())) { QApplication::restoreOverrideCursor(); QApplication::setOverrideCursor(QCursor(Qt::WaitCursor)); showStatusBarMessage("Retrieving history of '" + st.fileName() + "'..."); } } else if (fileTab->histListView->update() || st.sha().isEmpty()) { updateSpinBoxValue(); showStatusBarMessage(git->getRevInfo(st.sha())); } if (!fileTab->toolButtonPin->isChecked()) fileTab->textEditFile->doUpdate(); return true; // always accept new state } // ************************************ SLOTS ******************************** void FileView::updateEnabledButtons() { QToolButton* copy = fileTab->toolButtonCopy; QToolButton* showAnnotate = fileTab->toolButtonShowAnnotate; QToolButton* findAnnotate = fileTab->toolButtonFindAnnotate; QToolButton* goPrev = fileTab->toolButtonGoPrev; QToolButton* goNext = fileTab->toolButtonGoNext; QToolButton* rangeFilter = fileTab->toolButtonRangeFilter; QToolButton* highlight = fileTab->toolButtonHighlightText; bool fileAvailable = fileTab->textEditFile->isFileAvailable(); bool annotateAvailable = fileTab->textEditFile->isAnnotateAvailable(); // first enable copy->setEnabled(fileAvailable); showAnnotate->setEnabled(annotateAvailable); findAnnotate->setEnabled(annotateAvailable); goPrev->setEnabled(annotateAvailable); goNext->setEnabled(annotateAvailable); rangeFilter->setEnabled(annotateAvailable); highlight->setEnabled(fileAvailable && git->isTextHighlighter()); // then disable if (!showAnnotate->isChecked()) { findAnnotate->setEnabled(false); goPrev->setEnabled(false); goNext->setEnabled(false); } if (highlight->isChecked()) rangeFilter->setEnabled(false); if (rangeFilter->isChecked()) highlight->setEnabled(false); // special case: reset range filter when changing file if (!annotateAvailable && rangeFilter->isChecked()) rangeFilter->toggle(); } void FileView::on_toolButtonCopy_clicked() { fileTab->textEditFile->copySelection(); } void FileView::on_toolButtonShowAnnotate_toggled(bool b) { updateEnabledButtons(); fileTab->textEditFile->setShowAnnotate(b); if (b && fileTab->toolButtonFindAnnotate->isChecked()) goToCurrentAnnotation(); } void FileView::on_toolButtonFindAnnotate_toggled(bool b) { updateEnabledButtons(); if (b) goToCurrentAnnotation(); } void FileView::on_toolButtonGoNext_clicked() { goToCurrentAnnotation(1); } void FileView::on_toolButtonGoPrev_clicked() { goToCurrentAnnotation(-1); } void FileView::on_toolButtonPin_toggled(bool b) { // button is enabled and togglable only if st.sha() is found fileTab->spinBoxRevision->setDisabled(b); if (!b) { updateSpinBoxValue(); // UPDATE() call is filtered in this case fileTab->textEditFile->doUpdate(true); } } void FileView::on_toolButtonRangeFilter_toggled(bool b) { updateEnabledButtons(); if (b) { if (!fileTab->textEditFile->isAnnotateAvailable()) { dbs("ASSERT in on_toolButtonRangeFilter_toggled: annotate not available"); return; } if (!fileTab->textEditFile->textCursor().hasSelection()) { showStatusBarMessage("Please select some text"); return; } } bool rangeFilterActive = fileTab->textEditFile->rangeFilter(b); filterOnRange(rangeFilterActive); } void FileView::on_toolButtonHighlightText_toggled(bool b) { updateEnabledButtons(); fileTab->textEditFile->setHighlightSource(b); } void FileView::on_spinBoxRevision_valueChanged(int id) { if (id != fileTab->spinBoxRevision->minimum()) { SCRef selRev(fileTab->histListView->shaFromAnnId(id)); if (st.sha() != selRev) { // to avoid looping st.setSha(selRev); st.setSelectItem(true); UPDATE(); } } } void FileView::on_loadCompleted(const FileHistory* f, const QString& msg) { QApplication::restoreOverrideCursor(); if (f != model()) return; showStatusBarMessage(""); fileTab->histListView->showIdValues(); int maxId = model()->rowCount(); if (maxId == 0) return; fileTab->spinBoxRevision->setMaximum(maxId); fileTab->toolButtonPin->setEnabled(true); fileTab->spinBoxRevision->setEnabled(true); // update histListView now to avoid to miss // following status bar messages doUpdate(false); QString histTime = msg.section(" ms", 0, 0).section(" ", -1); if (fileTab->textEditFile->startAnnotate(model(), histTime)) showStatusBarMessage("Annotating revisions of '" + st.fileName() + "'..."); } void FileView::showAnnotation() { if ( !fileTab->toolButtonPin->isChecked() && fileTab->toolButtonShowAnnotate->isEnabled() && fileTab->toolButtonShowAnnotate->isChecked()) { fileTab->textEditFile->setShowAnnotate(true); if ( fileTab->toolButtonFindAnnotate->isEnabled() && fileTab->toolButtonFindAnnotate->isChecked()) goToCurrentAnnotation(); } } void FileView::on_annotationAvailable(bool b) { updateEnabledButtons(); if (b) showAnnotation(); // in case annotation got ready after file } void FileView::on_fileAvailable(bool b) { updateEnabledButtons(); if (b) { // code range is independent from annotation if (fileTab->toolButtonRangeFilter->isChecked()) fileTab->textEditFile->goToRangeStart(); showAnnotation(); // in case file got ready after annotation } } void FileView::on_revIdSelected(int id) { if (id == 0) return; if (fileTab->spinBoxRevision->isEnabled()) fileTab->spinBoxRevision->setValue(id); else { ListView* h = fileTab->histListView; int row = h->model()->rowCount() - id; QModelIndex idx = h->model()->index(row, 0); h->setCurrentIndex(idx); } } qgit-2.7/src/fileview.h000066400000000000000000000030671305655150700151150ustar00rootroot00000000000000/* Author: Marco Costalba (C) 2005-2007 Copyright: See COPYING file that comes with this distribution */ #ifndef FILEVIEW_H #define FILEVIEW_H #include "ui_fileview.h" // needed by moc_* file to understand tab() function #include "common.h" #include "domain.h" class MainImpl; class Git; class FileHistory; class FileView: public Domain { Q_OBJECT public: FileView() {} FileView(MainImpl* m, Git* git); ~FileView(); virtual void clear(bool complete = true); void append(SCRef data); void historyReady(); void updateHistViewer(SCRef revSha, SCRef fileName, bool fromUpstream = true); void eof(); Ui_TabFile* tab() { return fileTab; } public slots: void on_toolButtonCopy_clicked(); void on_toolButtonShowAnnotate_toggled(bool); void on_toolButtonFindAnnotate_toggled(bool); void on_toolButtonGoNext_clicked(); void on_toolButtonGoPrev_clicked(); void on_toolButtonRangeFilter_toggled(bool); void on_toolButtonPin_toggled(bool); void on_toolButtonHighlightText_toggled(bool); void on_spinBoxRevision_valueChanged(int); void on_loadCompleted(const FileHistory*, const QString&); void on_annotationAvailable(bool); void on_fileAvailable(bool); void on_revIdSelected(int); protected: virtual bool doUpdate(bool force); virtual bool isMatch(SCRef sha); virtual bool eventFilter(QObject *obj, QEvent *e); private: friend class MainImpl; friend class FileHighlighter; void showAnnotation(); bool goToCurrentAnnotation(int direction = 0); void filterOnRange(bool b); void updateSpinBoxValue(); void updateEnabledButtons(); Ui_TabFile* fileTab; }; #endif qgit-2.7/src/fileview.ui000066400000000000000000000317751305655150700153120ustar00rootroot00000000000000 TabFile 0 0 599 480 File 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 30 30 Copy selection to the clipboard stripping line headers (CTRL+C) :/icons/resources/editcopy.png Ctrl+C true false 30 30 Hide/show file annotation :/icons/resources/view_choose.png true true true false 30 30 Lock selection to first tagged line of current revision (CTRL + L) :/icons/resources/encrypted.png true true false Go to previous annotation line :/icons/resources/1uparrow.png true false Go to next annotation line :/icons/resources/1downarrow.png true false 30 30 Filter revisions on selected line range :/icons/resources/filter.png true true false 30 30 Source highlight (%1) :/icons/resources/colorize.png true true Qt::Horizontal QSizePolicy::Expanding 300 20 QFrame::StyledPanel QFrame::Raised 0 2 2 2 2 0 0 0 0 0 Pin View (Alt-V) :/icons/resources/ok.png true true 65535 Qt::Vertical Qt::CustomContextMenu true false true false true QFrame::StyledPanel QFrame::Sunken 0 0 0 0 0 1 0 0 0 0 QFrame::NoFrame Qt::ScrollBarAlwaysOff Qt::ScrollBarAlwaysOff QAbstractItemView::ScrollPerPixel QAbstractItemView::ScrollPerPixel QFrame::NoFrame false QTextEdit::NoWrap true ListView QTreeView
listview.h
FileContent QTextEdit
filecontent.h
qgit-2.7/src/git.cpp000066400000000000000000003000241305655150700144120ustar00rootroot00000000000000/* Description: interface to git programs Author: Marco Costalba (C) 2005-2007 Copyright: See COPYING file that comes with this distribution */ #include #include #include #include #include #include //#include //CT TODO remove #include #include #include #include #include "FileHistory.h" #include "annotate.h" #include "cache.h" #include "dataloader.h" #include "git.h" #include "lanes.h" #include "myprocess.h" #include "rangeselectimpl.h" #define SHOW_MSG(x) QApplication::postEvent(parent(), new MessageEvent(x)); EM_PROCESS_EVENTS_NO_INPUT; #define GIT_LOG_FORMAT "%m%HX%PX%n%cn<%ce>%n%an<%ae>%n%at%n%s%n" // Used on init() for reading parameters once; // It's OK to be unique among qgit windows. static bool startup = true; using namespace QGit; // **************************************************************************** bool Git::TreeEntry::operator<(const TreeEntry& te) const { if (this->type == te.type) return( this->name.localeAwareCompare( te.name ) < 0 ); // directories are smaller then files // to appear as first when sorted if (this->type == "tree") return true; if (te.type == "tree") return false; return( this->name.localeAwareCompare( te.name ) < 0 ); } Git::Git(QObject* p) : QObject(p) { EM_INIT(exGitStopped, "Stopping connection with git"); fileCacheAccessed = cacheNeedsUpdate = isMergeHead = false; isStGIT = isGIT = loadingUnAppliedPatches = isTextHighlighterFound = false; errorReportingEnabled = true; // report errors if run() fails curDomain = NULL; revData = NULL; revsFiles.reserve(MAX_DICT_SIZE); } void Git::checkEnvironment() { QString version; if (run("git --version", &version)) { version = version.section(' ', -1, -1).section('.', 0, 2); if (version < GIT_VERSION) { // simply send information, the 'not compatible version' // policy should be implemented upstream const QString cmd("Current git version is " + version + " but is required " + GIT_VERSION + " or better"); const QString errorDesc("Your installed git is too old." "\nPlease upgrade to avoid possible misbehaviours."); MainExecErrorEvent* e = new MainExecErrorEvent(cmd, errorDesc); QApplication::postEvent(parent(), e); } } else { dbs("Cannot find git files"); return; } errorReportingEnabled = false; isTextHighlighterFound = run("source-highlight -V", &version); errorReportingEnabled = true; if (isTextHighlighterFound) textHighlighterVersionFound = version.section('\n', 0, 0); else textHighlighterVersionFound = "GNU source-highlight not installed"; } void Git::userInfo(SList info) { /* git looks for commit user information in following order: - GIT_AUTHOR_NAME and GIT_AUTHOR_EMAIL environment variables - repository config file - global config file - your name, hostname and domain */ const QString env(QProcess::systemEnvironment().join(",")); QString user(env.section("GIT_AUTHOR_NAME", 1).section(",", 0, 0).section("=", 1).trimmed()); QString email(env.section("GIT_AUTHOR_EMAIL", 1).section(",", 0, 0).section("=", 1).trimmed()); info.clear(); info << "Environment" << user << email; errorReportingEnabled = false; // 'git config' could fail, see docs run("git config user.name", &user); run("git config user.email", &email); info << "Local config" << user << email; run("git config --global user.name", &user); run("git config --global user.email", &email); info << "Global config" << user << email; errorReportingEnabled = true; } const QStringList Git::getGitConfigList(bool global) { QString runOutput; errorReportingEnabled = false; // 'git config' could fail, see docs if (global) run("git config --global --list", &runOutput); else run("git config --list", &runOutput); errorReportingEnabled = true; return runOutput.split('\n', QString::SkipEmptyParts); } bool Git::isImageFile(SCRef file) { const QString ext(file.section('.', -1).toLower()); return QImageReader::supportedImageFormats().contains(ext.toLatin1()); } //CT TODO investigate if there is a better way of getting this (from git e.g.) bool Git::isBinaryFile(SCRef file) { static const char* binaryFileExtensions[] = {"bmp", "gif", "jpeg", "jpg", "png", "svg", "tiff", "pcx", "xcf", "xpm", "bz", "bz2", "rar", "tar", "z", "gz", "tgz", "zip", 0}; if (isImageFile(file)) return true; const QString ext(file.section('.', -1).toLower()); int i = 0; while (binaryFileExtensions[i] != 0) if (ext == binaryFileExtensions[i++]) return true; return false; } void Git::setThrowOnStop(bool b) { if (b) EM_REGISTER(exGitStopped); else EM_REMOVE(exGitStopped); } bool Git::isThrowOnStopRaised(int excpId, SCRef curContext) { return EM_MATCH(excpId, exGitStopped, curContext); } void Git::setTextCodec(QTextCodec* tc) { QTextCodec::setCodecForLocale(tc); QString name(tc ? tc->name() : "Latin1"); // workaround Qt issue of mime name different from // standard http://www.iana.org/assignments/character-sets if (name == "Big5-HKSCS") name = "Big5"; bool dummy; if (tc != getTextCodec(&dummy)) run("git config i18n.commitencoding " + name); } QTextCodec* Git::getTextCodec(bool* isGitArchive) { *isGitArchive = isGIT; if (!isGIT) // can be called also when not in an archive return NULL; QString runOutput; if (!run("git config i18n.commitencoding", &runOutput)) return NULL; if (runOutput.isEmpty()) // git docs says default is utf-8 return QTextCodec::codecForName(QByteArray("utf8")); return QTextCodec::codecForName(runOutput.trimmed().toLatin1()); } //CT TODO utility function; can go elsewhere const QString Git::quote(SCRef nm) { return (QUOTE_CHAR + nm + QUOTE_CHAR); } //CT TODO utility function; can go elsewhere const QString Git::quote(SCList sl) { QString q(sl.join(QUOTE_CHAR + ' ' + QUOTE_CHAR)); q.prepend(QUOTE_CHAR).append(QUOTE_CHAR); return q; } uint Git::checkRef(const ShaString& sha, uint mask) const { RefMap::const_iterator it(refsShaMap.constFind(sha)); return (it != refsShaMap.constEnd() ? (*it).type & mask : 0); } uint Git::checkRef(SCRef sha, uint mask) const { RefMap::const_iterator it(refsShaMap.constFind(toTempSha(sha))); return (it != refsShaMap.constEnd() ? (*it).type & mask : 0); } const QStringList Git::getRefName(SCRef sha, RefType type) const { if (!checkRef(sha, type)) return QStringList(); const Reference& rf = refsShaMap[toTempSha(sha)]; if (type == TAG) return rf.tags; else if (type == BRANCH) return rf.branches; else if (type == RMT_BRANCH) return rf.remoteBranches; else if (type == REF) return rf.refs; else if (type == APPLIED || type == UN_APPLIED) return QStringList(rf.stgitPatch); return QStringList(); } const QStringList Git::getAllRefSha(uint mask) { QStringList shas; FOREACH (RefMap, it, refsShaMap) if ((*it).type & mask) shas.append(it.key()); return shas; } const QString Git::getRefSha(SCRef refName, RefType type, bool askGit) { bool any = (type == ANY_REF); FOREACH (RefMap, it, refsShaMap) { const Reference& rf = *it; if ((any || type == TAG) && rf.tags.contains(refName)) return it.key(); else if ((any || type == BRANCH) && rf.branches.contains(refName)) return it.key(); else if ((any || type == RMT_BRANCH) && rf.remoteBranches.contains(refName)) return it.key(); else if ((any || type == REF) && rf.refs.contains(refName)) return it.key(); else if ((any || type == APPLIED || type == UN_APPLIED) && rf.stgitPatch == refName) return it.key(); } if (!askGit) return ""; // if a ref was not found perhaps is an abbreviated form QString runOutput; errorReportingEnabled = false; bool ok = run("git rev-parse --revs-only " + refName, &runOutput); errorReportingEnabled = true; return (ok ? runOutput.trimmed() : ""); } void Git::appendNamesWithId(QStringList& names, SCRef sha, SCList data, bool onlyLoaded) { const Rev* r = revLookup(sha); if (onlyLoaded && !r) return; if (onlyLoaded) { // prepare for later sorting SCRef cap = QString("%1 ").arg(r->orderIdx, 6); FOREACH_SL (it, data) names.append(cap + *it); } else names += data; } const QStringList Git::getAllRefNames(uint mask, bool onlyLoaded) { // returns reference names sorted by loading order if 'onlyLoaded' is set QStringList names; FOREACH (RefMap, it, refsShaMap) { if (mask & TAG) appendNamesWithId(names, it.key(), (*it).tags, onlyLoaded); if (mask & BRANCH) appendNamesWithId(names, it.key(), (*it).branches, onlyLoaded); if (mask & RMT_BRANCH) appendNamesWithId(names, it.key(), (*it).remoteBranches, onlyLoaded); if (mask & REF) appendNamesWithId(names, it.key(), (*it).refs, onlyLoaded); if ((mask & (APPLIED | UN_APPLIED)) && !onlyLoaded) names.append((*it).stgitPatch); // doesn't work with 'onlyLoaded' } if (onlyLoaded) { names.sort(); QStringList::iterator itN(names.begin()); for ( ; itN != names.end(); ++itN) // strip 'idx' (*itN) = (*itN).section(' ', -1, -1); } return names; } const QString Git::getRevInfo(SCRef sha) { if (sha.isEmpty()) return ""; uint type = checkRef(sha); if (type == 0) return ""; QString refsInfo; if (type & BRANCH) { const QString cap(type & CUR_BRANCH ? "HEAD: " : "Branch: "); refsInfo = cap + getRefName(sha, BRANCH).join(" "); } if (type & RMT_BRANCH) refsInfo.append(" Remote branch: " + getRefName(sha, RMT_BRANCH).join(" ")); if (type & TAG) refsInfo.append(" Tag: " + getRefName(sha, TAG).join(" ")); if (type & REF) refsInfo.append(" Ref: " + getRefName(sha, REF).join(" ")); if (type & APPLIED) refsInfo.append(" Patch: " + getRefName(sha, APPLIED).join(" ")); if (type & UN_APPLIED) refsInfo.append(" Patch: " + getRefName(sha, UN_APPLIED).join(" ")); if (type & TAG) { SCRef msg(getTagMsg(sha)); if (!msg.isEmpty()) refsInfo.append(" [" + msg + "]"); } return refsInfo.trimmed(); } const QString Git::getTagMsg(SCRef sha) { if (!checkRef(sha, TAG)) { dbs("ASSERT in Git::getTagMsg, tag not found"); return ""; } Reference& rf = refsShaMap[toTempSha(sha)]; if (!rf.tagMsg.isEmpty()) return rf.tagMsg; QRegExp pgp("-----BEGIN PGP SIGNATURE*END PGP SIGNATURE-----", Qt::CaseSensitive, QRegExp::Wildcard); if (!rf.tagObj.isEmpty()) { QString ro; if (run("git cat-file tag " + rf.tagObj, &ro)) rf.tagMsg = ro.section("\n\n", 1).remove(pgp).trimmed(); } return rf.tagMsg; } bool Git::isPatchName(SCRef nm) { if (!getRefSha(nm, UN_APPLIED, false).isEmpty()) return true; return !getRefSha(nm, APPLIED, false).isEmpty(); } void Git::addExtraFileInfo(QString* rowName, SCRef sha, SCRef diffToSha, bool allMergeFiles) { const RevFile* files = getFiles(sha, diffToSha, allMergeFiles); if (!files) return; int idx = findFileIndex(*files, *rowName); if (idx == -1) return; QString extSt(files->extendedStatus(idx)); if (extSt.isEmpty()) return; *rowName = extSt; } void Git::removeExtraFileInfo(QString* rowName) { if (rowName->contains(" --> ")) // return destination file name *rowName = rowName->section(" --> ", 1, 1).section(" (", 0, 0); } void Git::formatPatchFileHeader(QString* rowName, SCRef sha, SCRef diffToSha, bool combined, bool allMergeFiles) { if (combined) { rowName->prepend("diff --combined "); return; // TODO rename/copy still not supported in this case } // let's see if it's a rename/copy... addExtraFileInfo(rowName, sha, diffToSha, allMergeFiles); if (rowName->contains(" --> ")) { // ...it is! SCRef destFile(rowName->section(" --> ", 1, 1).section(" (", 0, 0)); SCRef origFile(rowName->section(" --> ", 0, 0)); *rowName = "diff --git a/" + origFile + " b/" + destFile; } else *rowName = "diff --git a/" + *rowName + " b/" + *rowName; } Annotate* Git::startAnnotate(FileHistory* fh, QObject* guiObj) { // non blocking Annotate* ann = new Annotate(this, guiObj); if (!ann->start(fh)) // non blocking call return NULL; // ann will delete itself when done return ann; // caller will delete with Git::cancelAnnotate() } void Git::cancelAnnotate(Annotate* ann) { if (ann) ann->deleteWhenDone(); } const FileAnnotation* Git::lookupAnnotation(Annotate* ann, SCRef sha) { return (ann ? ann->lookupAnnotation(sha) : NULL); } void Git::cancelDataLoading(const FileHistory* fh) { // normally called when closing file viewer emit cancelLoading(fh); // non blocking } const Rev* Git::revLookup(SCRef sha, const FileHistory* fh) const { return revLookup(toTempSha(sha), fh); } const Rev* Git::revLookup(const ShaString& sha, const FileHistory* fh) const { const RevMap& r = (fh ? fh->revs : revData->revs); return (sha.latin1() ? r.value(sha) : NULL); } bool Git::run(SCRef runCmd, QString* runOutput, QObject* receiver, SCRef buf) { QByteArray ba; bool ret = run(runOutput ? &ba : NULL, runCmd, receiver, buf); if (runOutput) *runOutput = ba; return ret; } bool Git::run(QByteArray* runOutput, SCRef runCmd, QObject* receiver, SCRef buf) { MyProcess p(parent(), this, workDir, errorReportingEnabled); return p.runSync(runCmd, runOutput, receiver, buf); } MyProcess* Git::runAsync(SCRef runCmd, QObject* receiver, SCRef buf) { MyProcess* p = new MyProcess(parent(), this, workDir, errorReportingEnabled); if (!p->runAsync(runCmd, receiver, buf)) { delete p; p = NULL; } return p; // auto-deleted when done } MyProcess* Git::runAsScript(SCRef runCmd, QObject* receiver, SCRef buf) { const QString scriptFile(workDir + "/qgit_script" + QGit::SCRIPT_EXT); #ifndef Q_OS_WIN32 // without this process doesn't start under Linux QString cmd(runCmd.startsWith("#!") ? runCmd : "#!/bin/sh\n" + runCmd); #else QString cmd(runCmd); #endif if (!writeToFile(scriptFile, cmd, true)) return NULL; MyProcess* p = runAsync(scriptFile, receiver, buf); if (p) connect(p, SIGNAL(eof()), this, SLOT(on_runAsScript_eof())); return p; } void Git::on_runAsScript_eof() { QDir dir(workDir); dir.remove("qgit_script" + QGit::SCRIPT_EXT); } void Git::cancelProcess(MyProcess* p) { if (p) p->on_cancel(); // non blocking call } int Git::findFileIndex(const RevFile& rf, SCRef name) { if (name.isEmpty()) return -1; int idx = name.lastIndexOf('/') + 1; SCRef dr = name.left(idx); SCRef nm = name.mid(idx); for (uint i = 0, cnt = rf.count(); i < cnt; ++i) { if (fileNamesVec[rf.nameAt(i)] == nm && dirNamesVec[rf.dirAt(i)] == dr) return i; } return -1; } const QString Git::getLaneParent(SCRef fromSHA, int laneNum) { const Rev* rs = revLookup(fromSHA); if (!rs) return ""; for (int idx = rs->orderIdx - 1; idx >= 0; idx--) { const Rev* r = revLookup(revData->revOrder[idx]); if (laneNum >= r->lanes.count()) return ""; if (!isFreeLane(r->lanes[laneNum])) { int type = r->lanes[laneNum], parNum = 0; while (!isMerge(type) && type != ACTIVE) { if (isHead(type)) parNum++; type = r->lanes[--laneNum]; } return r->parent(parNum); } } return ""; } const QStringList Git::getChildren(SCRef parent) { QStringList children; const Rev* r = revLookup(parent); if (!r) return children; for (int i = 0; i < r->children.count(); i++) children.append(revData->revOrder[r->children[i]]); // reorder children by loading order QStringList::iterator itC(children.begin()); for ( ; itC != children.end(); ++itC) { const Rev* r = revLookup(*itC); (*itC).prepend(QString("%1 ").arg(r->orderIdx, 6)); } children.sort(); for (itC = children.begin(); itC != children.end(); ++itC) (*itC) = (*itC).section(' ', -1, -1); return children; } const QString Git::getShortLog(SCRef sha) { const Rev* r = revLookup(sha); return (r ? r->shortLog() : ""); } MyProcess* Git::getDiff(SCRef sha, QObject* receiver, SCRef diffToSha, bool combined) { if (sha.isEmpty()) return NULL; QString runCmd; if (sha != ZERO_SHA) { runCmd = "git diff-tree --no-color -r --patch-with-stat "; runCmd.append(combined ? "-c " : "-C -m "); // TODO rename for combined runCmd.append(diffToSha + " " + sha); // diffToSha could be empty } else runCmd = "git diff-index --no-color -r -m --patch-with-stat HEAD"; return runAsync(runCmd, receiver); } const QString Git::getWorkDirDiff(SCRef fileName) { QString runCmd("git diff-index --no-color -r -z -m -p --full-index --no-commit-id HEAD"), runOutput; if (!fileName.isEmpty()) runCmd.append(" -- " + quote(fileName)); if (!run(runCmd, &runOutput)) return ""; /* For unknown reasons file sha of index is not ZERO_SHA but a value of unknown origin. Replace that with ZERO_SHA so to not fool annotate */ int idx = runOutput.indexOf(".."); if (idx != -1) runOutput.replace(idx + 2, 40, ZERO_SHA); return runOutput; } const QString Git::getFileSha(SCRef file, SCRef revSha) { if (revSha == ZERO_SHA) { QStringList files, dummy; getWorkDirFiles(files, dummy, RevFile::ANY); if (files.contains(file)) return ZERO_SHA; // it is unknown to git } const QString sha(revSha == ZERO_SHA ? "HEAD" : revSha); QString runCmd("git ls-tree -r " + sha + " " + quote(file)), runOutput; if (!run(runCmd, &runOutput)) return ""; return runOutput.mid(12, 40); // could be empty, deleted file case } MyProcess* Git::getFile(SCRef fileSha, QObject* receiver, QByteArray* result, SCRef fileName) { QString runCmd; /* symlinks in git are one line files with just the name of the target, not the target content. Instead 'cat' command resolves symlinks and returns target content. So we use 'cat' only if the file is modified in working directory, to let annotation work for changed files, otherwise we go with a safe 'git cat-file blob HEAD' instead. NOTE: This fails if the modified file is a new symlink, converted from an old plain file. In this case annotation will fail until change is committed. */ if (fileSha == ZERO_SHA) #ifdef Q_OS_WIN32 { QString winPath = quote(fileName); winPath.replace("/", "\\"); runCmd = "type " + winPath; } #else runCmd = "cat " + quote(fileName); #endif else { if (fileSha.isEmpty()) // deleted runCmd = "git diff-tree HEAD HEAD"; // fake an empty file reading else runCmd = "git cat-file blob " + fileSha; } if (!receiver) { run(result, runCmd); return NULL; // in case of sync call we ignore run() return value } return runAsync(runCmd, receiver); } MyProcess* Git::getHighlightedFile(SCRef fileSha, QObject* receiver, QString* result, SCRef fileName) { if (!isTextHighlighter()) { dbs("ASSERT in getHighlightedFile: highlighter not found"); return NULL; } QString ext(fileName.section('.', -1, -1, QString::SectionIncludeLeadingSep)); QString inputFile(workDir + "/qgit_hlght_input" + ext); if (!saveFile(fileSha, fileName, inputFile)) return NULL; QString runCmd("source-highlight --failsafe -f html -i " + quote(inputFile)); if (!receiver) { run(runCmd, result); on_getHighlightedFile_eof(); return NULL; // in case of sync call we ignore run() return value } MyProcess* p = runAsync(runCmd, receiver); if (p) connect(p, SIGNAL(eof()), this, SLOT(on_getHighlightedFile_eof())); return p; } void Git::on_getHighlightedFile_eof() { QDir dir(workDir); const QStringList sl(dir.entryList(QStringList() << "qgit_hlght_input*")); FOREACH_SL (it, sl) dir.remove(*it); } bool Git::saveFile(SCRef fileSha, SCRef fileName, SCRef path) { QByteArray fileData; getFile(fileSha, NULL, &fileData, fileName); // sync call if (isBinaryFile(fileName)) return writeToFile(path, fileData); return writeToFile(path, QString(fileData)); } bool Git::getTree(SCRef treeSha, TreeInfo& ti, bool isWorkingDir, SCRef path) { QStringList deleted; if (isWorkingDir) { // retrieve unknown and deleted files under path QStringList unknowns, dummy; getWorkDirFiles(unknowns, dummy, RevFile::UNKNOWN); FOREACH_SL (it, unknowns) { // don't add files under other directories QFileInfo f(*it); SCRef d(f.dir().path()); if (d == path || (path.isEmpty() && d == ".")) { TreeEntry te(f.fileName(), "", "?"); ti.append(te); } } getWorkDirFiles(deleted, dummy, RevFile::DELETED); } // if needed fake a working directory tree starting from HEAD tree QString runOutput, tree(treeSha); if (treeSha == ZERO_SHA) { // HEAD could be empty for just init'ed repositories if (!run("git rev-parse --revs-only HEAD", &tree)) return false; tree = tree.trimmed(); } if (!tree.isEmpty() && !run("git ls-tree " + tree, &runOutput)) return false; const QStringList sl(runOutput.split('\n', QString::SkipEmptyParts)); FOREACH_SL (it, sl) { // append any not deleted file SCRef fn((*it).section('\t', 1, 1)); SCRef fp(path.isEmpty() ? fn : path + '/' + fn); if (deleted.empty() || (deleted.indexOf(fp) == -1)) { TreeEntry te(fn, (*it).mid(12, 40), (*it).mid(7, 4)); ti.append(te); } } qSort(ti); // list directories before files return true; } void Git::getWorkDirFiles(SList files, SList dirs, RevFile::StatusFlag status) { files.clear(); dirs.clear(); const RevFile* f = getFiles(ZERO_SHA); if (!f) return; for (int i = 0; i < f->count(); i++) { if (f->statusCmp(i, status)) { SCRef fp(filePath(*f, i)); files.append(fp); for (int j = 0, cnt = fp.count('/'); j < cnt; j++) { SCRef dir(fp.section('/', 0, j)); if (dirs.indexOf(dir) == -1) dirs.append(dir); } } } } bool Git::isNothingToCommit() { if (!revsFiles.contains(ZERO_SHA_RAW)) return true; const RevFile* rf = revsFiles[ZERO_SHA_RAW]; return (rf->count() == workingDirInfo.otherFiles.count()); } bool Git::isTreeModified(SCRef sha) { const RevFile* f = getFiles(sha); if (!f) return true; // no files info, stay on the safe side for (int i = 0; i < f->count(); ++i) if (!f->statusCmp(i, RevFile::MODIFIED)) return true; return false; } bool Git::isParentOf(SCRef par, SCRef child) { const Rev* c = revLookup(child); return (c && c->parentsCount() == 1 && QString(c->parent(0)) == par); // no merges } bool Git::isSameFiles(SCRef tree1Sha, SCRef tree2Sha) { // early skip common case of browsing with up and down arrows, i.e. // going from parent(child) to child(parent). In this case we can // check RevFileMap and skip a costly 'git diff-tree' call. if (isParentOf(tree1Sha, tree2Sha)) return !isTreeModified(tree2Sha); if (isParentOf(tree2Sha, tree1Sha)) return !isTreeModified(tree1Sha); const QString runCmd("git diff-tree --no-color -r " + tree1Sha + " " + tree2Sha); QString runOutput; if (!run(runCmd, &runOutput)) return false; bool isChanged = (runOutput.indexOf(" A\t") != -1 || runOutput.indexOf(" D\t") != -1); return !isChanged; } const QStringList Git::getDescendantBranches(SCRef sha, bool shaOnly) { QStringList tl; const Rev* r = revLookup(sha); if (!r || (r->descBrnMaster == -1)) return tl; const QVector& nr = revLookup(revData->revOrder[r->descBrnMaster])->descBranches; for (int i = 0; i < nr.count(); i++) { const ShaString& sha = revData->revOrder[nr[i]]; if (shaOnly) { tl.append(sha); continue; } SCRef cap = " (" + sha + ") "; RefMap::const_iterator it(refsShaMap.find(sha)); if (it == refsShaMap.constEnd()) continue; if (!(*it).branches.empty()) tl.append((*it).branches.join(" ").append(cap)); if (!(*it).remoteBranches.empty()) tl.append((*it).remoteBranches.join(" ").append(cap)); } return tl; } const QStringList Git::getNearTags(bool goDown, SCRef sha) { QStringList tl; const Rev* r = revLookup(sha); if (!r) return tl; int nearRefsMaster = (goDown ? r->descRefsMaster : r->ancRefsMaster); if (nearRefsMaster == -1) return tl; const QVector& nr = goDown ? revLookup(revData->revOrder[nearRefsMaster])->descRefs : revLookup(revData->revOrder[nearRefsMaster])->ancRefs; for (int i = 0; i < nr.count(); i++) { const ShaString& sha = revData->revOrder[nr[i]]; SCRef cap = " (" + sha + ")"; RefMap::const_iterator it(refsShaMap.find(sha)); if (it != refsShaMap.constEnd()) tl.append((*it).tags.join(cap).append(cap)); } return tl; } const QString Git::getLastCommitMsg() { // FIXME: Make sure the amend action is not called when there is // nothing to amend. That is in empty repository or over StGit stack // with nothing applied. QString sha; QString top; if (run("git rev-parse --verify HEAD", &top)) sha = top.trimmed(); else { dbs("ASSERT: getLastCommitMsg head is not valid"); return ""; } const Rev* c = revLookup(sha); if (!c) { dbp("ASSERT: getLastCommitMsg sha <%1> not found", sha); return ""; } return c->shortLog() + "\n\n" + c->longLog().trimmed(); } const QString Git::getNewCommitMsg() { const Rev* c = revLookup(ZERO_SHA); if (!c) { dbs("ASSERT: getNewCommitMsg zero_sha not found"); return ""; } QString status = c->longLog(); status.prepend('\n').replace(QRegExp("\\n([^#\\n]?)"), "\n#\\1"); // comment all the lines if (isMergeHead) { QFile file(QDir(gitDir).absoluteFilePath("MERGE_MSG")); if (file.open(QIODevice::ReadOnly)) { QTextStream in(&file); while(!in.atEnd()) status.prepend(in.readLine()); file.close(); } } return status; } //CT TODO utility function; can go elsewhere const QString Git::colorMatch(SCRef txt, QRegExp& regExp) { QString text = qt4and5escaping(txt); if (regExp.isEmpty()) return text; SCRef startCol(QString::fromLatin1("")); SCRef endCol(QString::fromLatin1("")); int pos = 0; while ((pos = text.indexOf(regExp, pos)) != -1) { SCRef match(regExp.cap(0)); const QString coloredText(startCol + match + endCol); text.replace(pos, match.length(), coloredText); pos += coloredText.length(); } return text; } //CT TODO utility function; can go elsewhere const QString Git::formatList(SCList sl, SCRef name, bool inOneLine) { if (sl.isEmpty()) return QString(); QString ls = "" + name + ""; const QString joinStr = inOneLine ? ", " : "\n" + ls; ls += sl.join(joinStr); ls += "\n"; return ls; } const QString Git::getDesc(SCRef sha, QRegExp& shortLogRE, QRegExp& longLogRE, bool showHeader, FileHistory* fh) { if (sha.isEmpty()) return ""; const Rev* c = revLookup(sha, fh); if (!c) // sha of a not loaded revision, as return ""; // example asked from file history QString text; if (c->isDiffCache) text = Qt::convertFromPlainText(c->longLog()); else { QTextStream ts(&text); ts << "
\n" ""; ts << ""; if (showHeader) { if (c->committer() != c->author()) ts << formatList(QStringList(qt4and5escaping(c->committer())), "Committer"); ts << formatList(QStringList(qt4and5escaping(c->author())), "Author"); ts << formatList(QStringList(getLocalDate(c->authorDate())), " Author date"); if (c->isUnApplied || c->isApplied) { QStringList patches(getRefName(sha, APPLIED)); patches += getRefName(sha, UN_APPLIED); ts << formatList(patches, "Patch"); } else { ts << formatList(c->parents(), "Parent", false); ts << formatList(getChildren(sha), "Child", false); ts << formatList(getDescendantBranches(sha), "Branch", false); ts << formatList(getNearTags(!optGoDown, sha), "Follows"); ts << formatList(getNearTags(optGoDown, sha), "Precedes"); } } QString longLog(c->longLog()); if (showHeader) { longLog.prepend(QString("\n") + c->shortLog() + "\n"); } QString log(colorMatch(longLog, longLogRE)); log.replace("\n", "\n ").prepend('\n'); ts << "
" << colorMatch(c->shortLog(), shortLogRE) << "
" << log << "
"; } // highlight SHA's // // added to commit logs, we avoid to call git rev-parse for a possible abbreviated // sha if there isn't a leading trailing space or an open parenthesis and, // in that case, before the space must not be a ':' character. // It's an ugly heuristic, but seems to work in most cases. QRegExp reSHA("..[0-9a-f]{21,40}|[^:][\\s(][0-9a-f]{6,20}", Qt::CaseInsensitive); reSHA.setMinimal(false); int pos = 0; while ((pos = text.indexOf(reSHA, pos)) != -1) { SCRef ref = reSHA.cap(0).mid(2); const Rev* r = (ref.length() == 40 ? revLookup(ref) : revLookup(getRefSha(ref))); if (r && r->sha() != ZERO_SHA_RAW) { QString slog(r->shortLog()); if (slog.isEmpty()) // very rare but possible slog = r->sha(); if (slog.length() > 60) slog = slog.left(57).trimmed().append("..."); const QString link("sha() + "\">" + qt4and5escaping(slog) + ""); text.replace(pos + 2, ref.length(), link); pos += link.length(); } else pos += reSHA.cap(0).length(); } return text; } const RevFile* Git::insertNewFiles(SCRef sha, SCRef data) { /* we use an independent FileNamesLoader to avoid data * corruption if we are loading file names in background */ FileNamesLoader fl; RevFile* rf = new RevFile(); parseDiffFormat(*rf, data, fl); flushFileNames(fl); revsFiles.insert(toPersistentSha(sha, revsFilesShaBackupBuf), rf); return rf; } bool Git::runDiffTreeWithRenameDetection(SCRef runCmd, QString* runOutput) { /* Under some cases git could warn out: "too many files, skipping inexact rename detection" So if this occurs fallback on NO rename detection. */ QString cmd(runCmd); // runCmd must be without -C option cmd.replace("git diff-tree", "git diff-tree -C"); errorReportingEnabled = false; bool renameDetectionOk = run(cmd, runOutput); errorReportingEnabled = true; if (!renameDetectionOk) // retry without rename detection return run(runCmd, runOutput); return true; } const RevFile* Git::getAllMergeFiles(const Rev* r) { SCRef mySha(ALL_MERGE_FILES + r->sha()); if (revsFiles.contains(toTempSha(mySha))) return revsFiles[toTempSha(mySha)]; EM_PROCESS_EVENTS; // 'git diff-tree' could be slow QString runCmd("git diff-tree --no-color -r -m " + r->sha()), runOutput; if (!runDiffTreeWithRenameDetection(runCmd, &runOutput)) return NULL; return insertNewFiles(mySha, runOutput); } const RevFile* Git::getFiles(SCRef sha, SCRef diffToSha, bool allFiles, SCRef path) { const Rev* r = revLookup(sha); if (!r) return NULL; if (r->parentsCount() == 0) // skip initial rev return NULL; if (r->parentsCount() > 1 && diffToSha.isEmpty() && allFiles) return getAllMergeFiles(r); if (!diffToSha.isEmpty() && (sha != ZERO_SHA)) { QString runCmd("git diff-tree --no-color -r -m "); runCmd.append(diffToSha + " " + sha); if (!path.isEmpty()) runCmd.append(" " + path); EM_PROCESS_EVENTS; // 'git diff-tree' could be slow QString runOutput; if (!runDiffTreeWithRenameDetection(runCmd, &runOutput)) return NULL; // we insert a dummy revision file object. It will be // overwritten at each request but we don't care. return insertNewFiles(CUSTOM_SHA, runOutput); } if (revsFiles.contains(r->sha())) return revsFiles[r->sha()]; // ZERO_SHA search arrives here if (sha == ZERO_SHA) { dbs("ASSERT in Git::getFiles, ZERO_SHA not found"); return NULL; } EM_PROCESS_EVENTS; // 'git diff-tree' could be slow QString runCmd("git diff-tree --no-color -r -c " + sha), runOutput; if (!runDiffTreeWithRenameDetection(runCmd, &runOutput)) return NULL; if (revsFiles.contains(r->sha())) // has been created in the mean time? return revsFiles[r->sha()]; cacheNeedsUpdate = true; return insertNewFiles(sha, runOutput); } bool Git::startFileHistory(SCRef sha, SCRef startingFileName, FileHistory* fh) { QStringList args(getDescendantBranches(sha, true)); if (args.isEmpty()) args << "HEAD"; QString newestFileName = getNewestFileName(args, startingFileName); fh->resetFileNames(newestFileName); args.clear(); // load history from all the branches args << getAllRefSha(BRANCH | RMT_BRANCH); args << "--" << newestFileName; return startRevList(args, fh); } const QString Git::getNewestFileName(SCList branches, SCRef fileName) { QString curFileName(fileName), runOutput, args; while (true) { args = branches.join(" ") + " -- " + curFileName; if (!run("git ls-tree " + args, &runOutput)) break; if (!runOutput.isEmpty()) break; QString msg("Retrieving file renames, now at '" + curFileName + "'..."); QApplication::postEvent(parent(), new MessageEvent(msg)); EM_PROCESS_EVENTS_NO_INPUT; if (!run("git rev-list -n1 " + args, &runOutput)) break; if (runOutput.isEmpty()) // try harder if (!run("git rev-list --full-history -n1 " + args, &runOutput)) break; if (runOutput.isEmpty()) break; SCRef sha = runOutput.trimmed(); QStringList newCur; if (!populateRenamedPatches(sha, QStringList(curFileName), NULL, &newCur, true)) break; curFileName = newCur.first(); } return curFileName; } void Git::getFileFilter(SCRef path, ShaSet& shaSet) const { shaSet.clear(); QRegExp rx(path, Qt::CaseInsensitive, QRegExp::Wildcard); FOREACH (ShaVect, it, revData->revOrder) { if (!revsFiles.contains(*it)) continue; // case insensitive, wildcard search const RevFile* rf = revsFiles[*it]; for (int i = 0; i < rf->count(); ++i) if (filePath(*rf, i).contains(rx)) { shaSet.insert(*it); break; } } } bool Git::getPatchFilter(SCRef exp, bool isRegExp, ShaSet& shaSet) { shaSet.clear(); QString buf; FOREACH (ShaVect, it, revData->revOrder) if (*it != ZERO_SHA_RAW) buf.append(*it).append('\n'); if (buf.isEmpty()) return true; EM_PROCESS_EVENTS; // 'git diff-tree' could be slow QString runCmd("git diff-tree --no-color -r -s --stdin "), runOutput; if (isRegExp) runCmd.append("--pickaxe-regex "); runCmd.append(quote("-S" + exp)); if (!run(runCmd, &runOutput, NULL, buf)) return false; const QStringList sl(runOutput.split('\n', QString::SkipEmptyParts)); FOREACH_SL (it, sl) shaSet.insert(*it); return true; } bool Git::resetCommits(int parentDepth) { QString runCmd("git reset --soft HEAD~"); runCmd.append(QString::number(parentDepth)); return run(runCmd); } bool Git::applyPatchFile(SCRef patchPath, bool fold, bool isDragDrop) { if (isStGIT) { if (fold) { bool ok = run("stg fold " + quote(patchPath)); // merge in working directory if (ok) ok = run("stg refresh"); // update top patch return ok; } else return run("stg import --mail " + quote(patchPath)); } QString runCmd("git am --utf8 --3way "); QSettings settings; const QString APOpt(settings.value(AM_P_OPT_KEY).toString()); if (!APOpt.isEmpty()) runCmd.append(APOpt.trimmed() + " "); if (isDragDrop) runCmd.append("--keep "); if (testFlag(SIGN_PATCH_F)) runCmd.append("--signoff "); return run(runCmd + quote(patchPath)); } const QStringList Git::sortShaListByIndex(SCList shaList) { QStringList orderedShaList; FOREACH_SL (it, shaList) appendNamesWithId(orderedShaList, *it, QStringList(*it), true); orderedShaList.sort(); QStringList::iterator itN(orderedShaList.begin()); for ( ; itN != orderedShaList.end(); ++itN) // strip 'idx' (*itN) = (*itN).section(' ', -1, -1); return orderedShaList; } bool Git::formatPatch(SCList shaList, SCRef dirPath, SCRef remoteDir) { bool remote = !remoteDir.isEmpty(); QSettings settings; const QString FPOpt(settings.value(FMT_P_OPT_KEY).toString()); QString runCmd("git format-patch --no-color"); if (testFlag(NUMBERS_F) && !remote) runCmd.append(" -n"); if (remote) runCmd.append(" --keep-subject"); runCmd.append(" -o " + quote(dirPath)); if (!FPOpt.isEmpty()) runCmd.append(" " + FPOpt.trimmed()); const QString tmp(workDir); if (remote) workDir = remoteDir; // run() uses workDir value // shaList is ordered by newest to oldest runCmd.append(" " + shaList.last()); runCmd.append(QString::fromLatin1("^..") + shaList.first()); bool ret = run(runCmd); workDir = tmp; return ret; } const QStringList Git::getOtherFiles(SCList selFiles, bool onlyInIndex) { const RevFile* files = getFiles(ZERO_SHA); // files != NULL QStringList notSelFiles; for (int i = 0; i < files->count(); ++i) { SCRef fp = filePath(*files, i); if (selFiles.indexOf(fp) == -1) { // not selected... if (!onlyInIndex || files->statusCmp(i, RevFile::IN_INDEX)) notSelFiles.append(fp); } } return notSelFiles; } bool Git::updateIndex(SCList selFiles) { const RevFile* files = getFiles(ZERO_SHA); // files != NULL QStringList toAdd, toRemove; FOREACH_SL (it, selFiles) { int idx = findFileIndex(*files, *it); if (files->statusCmp(idx, RevFile::DELETED)) toRemove << *it; else toAdd << *it; } if (!toRemove.isEmpty() && !run("git rm --cached --ignore-unmatch -- " + quote(toRemove))) return false; if (!toAdd.isEmpty() && !run("git add -- " + quote(toAdd))) return false; return true; } bool Git::commitFiles(SCList selFiles, SCRef msg, bool amend) { const QString msgFile(gitDir + "/qgit_cmt_msg.txt"); if (!writeToFile(msgFile, msg)) // early skip return false; // add user selectable commit options QSettings settings; const QString CMArgs(settings.value(CMT_ARGS_KEY).toString()); QString cmtOptions; if (!CMArgs.isEmpty()) cmtOptions.append(" " + CMArgs); if (testFlag(SIGN_CMT_F)) cmtOptions.append(" -s"); if (testFlag(VERIFY_CMT_F)) cmtOptions.append(" -v"); if (amend) cmtOptions.append(" --amend"); bool ret = false; // get not selected files but updated in index to restore at the end const QStringList notSel(getOtherFiles(selFiles, optOnlyInIndex)); // call git reset to remove not selected files from index if (!notSel.empty() && !run("git reset -- " + quote(notSel))) goto fail; // update index with selected files if (!updateIndex(selFiles)) goto fail; // now we can finally commit.. if (!run("git commit" + cmtOptions + " -F " + quote(msgFile))) goto fail; // restore not selected files that were already in index if (!notSel.empty() && !updateIndex(notSel)) goto fail; ret = true; fail: QDir dir(workDir); dir.remove(msgFile); return ret; } bool Git::mkPatchFromWorkDir(SCRef msg, SCRef patchFile, SCList files) { /* unfortunately 'git diff' sees only files already * known to git or already in index, so update index first * to be sure also unknown files are correctly found */ if (!updateIndex(files)) return false; QString runOutput; if (!run("git diff --no-ext-diff -C HEAD -- " + quote(files), &runOutput)) return false; const QString patch("Subject: " + msg + "\n---\n" + runOutput); return writeToFile(patchFile, patch); } bool Git::stgCommit(SCList selFiles, SCRef msg, SCRef patchName, bool fold) { /* Here the deal is to use 'stg import' and 'stg fold' to add a new * patch or refresh the current one respectively. Unfortunately refresh * does not work with partial selection of files and also does not take * patch message from a file that is needed to avoid artifacts with '\n' * and friends. * * So steps are: * * - Create a patch file with the changes you want to import/fold in StGit * - Stash working directory files because import/fold wants a clean directory * - Import/fold the patch * - Unstash and merge working directory modified files * - Restore index with not selected files */ /* Step 1: Create a patch file with the changes you want to import/fold */ bool ret = false; const QString patchFile(gitDir + "/qgit_tmp_patch.txt"); // in case we don't have files to restore we can shortcut various commands bool partialSelection = !getOtherFiles(selFiles, !optOnlyInIndex).isEmpty(); // get not selected files but updated in index to restore at the end QStringList notSel; if (partialSelection) // otherwise notSel is for sure empty notSel = getOtherFiles(selFiles, optOnlyInIndex); // create a patch with diffs between working directory and HEAD if (!mkPatchFromWorkDir(msg, patchFile, selFiles)) goto fail; /* Step 2: Stash working directory modified files */ if (partialSelection) { errorReportingEnabled = false; run("git stash"); // unfortunately 'git stash' is noisy on stderr errorReportingEnabled = true; } /* Step 3: Call stg import/fold */ // setup a clean state if (!run("stg status --reset")) goto fail_and_unstash; if (fold) { // update patch message before to fold, note that // command 'stg edit' requires stg version 0.14 or later if (!msg.isEmpty() && !run("stg edit --message " + quote(msg.trimmed()))) goto fail_and_unstash; if (!run("stg fold " + quote(patchFile))) goto fail_and_unstash; if (!run("stg refresh")) // refresh needed after fold goto fail_and_unstash; } else if (!run("stg import --mail --name " + quote(patchName) + " " + quote(patchFile))) goto fail_and_unstash; if (partialSelection) { /* Step 4: Unstash and merge working directory modified files */ errorReportingEnabled = false; run("git stash pop"); // unfortunately 'git stash' is noisy on stderr errorReportingEnabled = true; /* Step 5: restore not selected files that were already in index */ if (!notSel.empty() && !updateIndex(notSel)) goto fail; } ret = true; goto exit; fail_and_unstash: if (partialSelection) { run("git reset"); errorReportingEnabled = false; run("git stash pop"); errorReportingEnabled = true; } fail: exit: QDir dir(workDir); dir.remove(patchFile); return ret; } bool Git::stgPush(SCRef sha) { const QStringList patch(getRefName(sha, UN_APPLIED)); if (patch.count() != 1) { dbp("ASSERT in Git::stgPush, found %1 patches instead of 1", patch.count()); return false; } return run("stg push " + quote(patch.first())); } bool Git::stgPop(SCRef sha) { const QStringList patch(getRefName(sha, APPLIED)); if (patch.count() != 1) { dbp("ASSERT in Git::stgPop, found %1 patches instead of 1", patch.count()); return false; } return run("stg pop " + quote(patch)); } //! cache for dates conversion. Common among qgit windows static QHash localDates; /** * Accesses a cache that avoids slow date calculation * * @param gitDate * the reference from which we want to get the date * * @return * human-readable date **/ const QString Git::getLocalDate(SCRef gitDate) { QString localDate(localDates.value(gitDate)); // cache miss if (localDate.isEmpty()) { static QDateTime d; d.setTime_t(gitDate.toUInt()); localDate = d.toString(Qt::SystemLocaleShortDate); // save to cache localDates[gitDate] = localDate; } return localDate; } const QStringList Git::getArgs(bool* quit, bool repoChanged) { QString args; QStringList arglist = qApp->arguments(); // Remove first argument which is the path of the current executable arglist.removeFirst(); if (startup) { foreach (QString arg, arglist) { // in arguments with spaces double quotes // are stripped by Qt, so re-add them if (arg.contains(' ')) arg.prepend('\"').append('\"'); args.append(arg + ' '); } } if (!startup || args.isEmpty()) { // need to retrieve args if (testFlag(RANGE_SELECT_F)) { // open range dialog RangeSelectImpl rs((QWidget*)parent(), &args, repoChanged, this); *quit = (rs.exec() == QDialog::Rejected); // modal execution if (*quit) return QStringList(); } else { args = RangeSelectImpl::getDefaultArgs(); } } startup = false; return MyProcess::splitArgList(args); } bool Git::getGitDBDir(SCRef wd, QString& gd, bool& changed) { // we could run from a subdirectory, so we need to get correct directories QString runOutput, tmp(workDir); workDir = wd; errorReportingEnabled = false; bool success = run("git rev-parse --git-dir", &runOutput); // run under newWorkDir errorReportingEnabled = true; workDir = tmp; runOutput = runOutput.trimmed(); if (success) { // 'git rev-parse --git-dir' output could be a relative // to working directory (as ex .git) or an absolute path QDir d(runOutput.startsWith("/") ? runOutput : wd + "/" + runOutput); changed = (d.absolutePath() != gitDir); gd = d.absolutePath(); } return success; } bool Git::getBaseDir(SCRef wd, QString& bd, bool& changed) { // we could run from a subdirectory, so we need to get correct directories // We use --show-cdup and not --git-dir for this, in order to take into account configurations // in which .git is indeed a "symlink", a text file containing the path of the actual .git database dir. // In that particular case, the parent directory of the one given by --git-dir is *not* necessarily // the base directory of the repository. QString runOutput, tmp(workDir); workDir = wd; errorReportingEnabled = false; bool success = run("git rev-parse --show-cdup", &runOutput); // run under newWorkDir errorReportingEnabled = true; workDir = tmp; runOutput = runOutput.trimmed(); if (success) { // 'git rev-parse --show-cdup' is relative to working directory. QDir d(wd + "/" + runOutput); bd = d.absolutePath(); changed = (bd != workDir); } else { changed = true; bd = wd; } return success; } Git::Reference* Git::lookupOrAddReference(const ShaString& sha) { RefMap::iterator it(refsShaMap.find(sha)); if (it == refsShaMap.end()) it = refsShaMap.insert(sha, Reference()); return &(*it); } Git::Reference* Git::lookupReference(const ShaString& sha) { RefMap::iterator it(refsShaMap.find(sha)); if (it == refsShaMap.end()) return 0; return &(*it); } bool Git::getRefs() { // check for a StGIT stack QDir d(gitDir); QString stgCurBranch; if (d.exists("patches")) { // early skip errorReportingEnabled = false; isStGIT = run("stg branch", &stgCurBranch); // slow command errorReportingEnabled = true; stgCurBranch = stgCurBranch.trimmed(); } else isStGIT = false; // check for a merge and read current branch sha isMergeHead = d.exists("MERGE_HEAD"); QString curBranchSHA; if (!run("git rev-parse --revs-only HEAD", &curBranchSHA)) return false; if (!run("git branch", &curBranchName)) return false; curBranchSHA = curBranchSHA.trimmed(); curBranchName = curBranchName.prepend('\n').section("\n*", 1); curBranchName = curBranchName.section('\n', 0, 0).trimmed(); if (curBranchName.startsWith("(detached from")) curBranchName = ""; // read refs, normally unsorted QString runOutput; if (!run("git show-ref -d", &runOutput)) return false; refsShaMap.clear(); shaBackupBuf.clear(); // revs are already empty now QString prevRefSha; QStringList patchNames, patchShas; const QStringList rLst(runOutput.split('\n', QString::SkipEmptyParts)); FOREACH_SL (it, rLst) { SCRef revSha = (*it).left(40); SCRef refName = (*it).mid(41); if (refName.startsWith("refs/patches/")) { // save StGIT patch sha, to be used later SCRef patchesDir("refs/patches/" + stgCurBranch + "/"); if (refName.startsWith(patchesDir)) { patchNames.append(refName.mid(patchesDir.length())); patchShas.append(revSha); } // StGIT patches should not be added to refs, // but an applied StGIT patch could be also an head or // a tag in this case will be added in another loop cycle continue; } // one rev could have many tags Reference* cur = lookupOrAddReference(toPersistentSha(revSha, shaBackupBuf)); if (refName.startsWith("refs/tags/")) { if (refName.endsWith("^{}")) { // tag dereference // we assume that a tag dereference follows strictly // the corresponding tag object in rLst. So the // last added tag is a tag object, not a commit object cur->tags.append(refName.mid(10, refName.length() - 13)); // store tag object. Will be used to fetching // tag message (if any) when necessary. cur->tagObj = prevRefSha; // tagObj must be removed from ref map if (!prevRefSha.isEmpty()) refsShaMap.remove(toTempSha(prevRefSha)); } else cur->tags.append(refName.mid(10)); cur->type |= TAG; } else if (refName.startsWith("refs/heads/")) { cur->branches.append(refName.mid(11)); cur->type |= BRANCH; if (curBranchSHA == revSha) cur->type |= CUR_BRANCH; } else if (refName.startsWith("refs/remotes/") && !refName.endsWith("HEAD")) { cur->remoteBranches.append(refName.mid(13)); cur->type |= RMT_BRANCH; } else if (!refName.startsWith("refs/bases/") && !refName.endsWith("HEAD")) { cur->refs.append(refName); cur->type |= REF; } prevRefSha = revSha; } if (isStGIT && !patchNames.isEmpty()) parseStGitPatches(patchNames, patchShas); // mark current head (even when detached) Reference* cur = lookupOrAddReference(toPersistentSha(curBranchSHA, shaBackupBuf)); cur->type |= CUR_BRANCH; return !refsShaMap.empty(); } void Git::parseStGitPatches(SCList patchNames, SCList patchShas) { patchesStillToFind = 0; // get patch names and status of current branch QString runOutput; if (!run("stg series", &runOutput)) return; const QStringList pl(runOutput.split('\n', QString::SkipEmptyParts)); FOREACH_SL (it, pl) { SCRef status = (*it).left(1); SCRef patchName = (*it).mid(2); bool applied = (status == "+" || status == ">"); int pos = patchNames.indexOf(patchName); if (pos == -1) { dbp("ASSERT in Git::parseStGitPatches(), patch %1 " "not found in references list.", patchName); continue; } const ShaString& ss = toPersistentSha(patchShas.at(pos), shaBackupBuf); Reference* cur = lookupOrAddReference(ss); cur->stgitPatch = patchName; cur->type |= (applied ? APPLIED : UN_APPLIED); if (applied) patchesStillToFind++; } } const QStringList Git::getOthersFiles() { // add files present in working directory but not in git archive QString runCmd("git ls-files --others"); QSettings settings; QString exFile(settings.value(EX_KEY, EX_DEF).toString()); if (!exFile.isEmpty()) { QString path = (exFile.startsWith("/")) ? exFile : workDir + "/" + exFile; if (QFile::exists(path)) runCmd.append(" --exclude-from=" + quote(exFile)); } QString exPerDir(settings.value(EX_PER_DIR_KEY, EX_PER_DIR_DEF).toString()); if (!exPerDir.isEmpty()) runCmd.append(" --exclude-per-directory=" + quote(exPerDir)); QString runOutput; run(runCmd, &runOutput); return runOutput.split('\n', QString::SkipEmptyParts); } Rev* Git::fakeRevData(SCRef sha, SCList parents, SCRef author, SCRef date, SCRef log, SCRef longLog, SCRef patch, int idx, FileHistory* fh) { QString data('>' + sha + 'X' + parents.join(" ") + " \n"); data.append(author + '\n' + author + '\n' + date + '\n'); data.append(log + '\n' + longLog); QString header("log size " + QString::number(QByteArray(data.toLatin1()).length() - 1) + '\n'); data.prepend(header); if (!patch.isEmpty()) data.append('\n' + patch); #if QT_VERSION >= 0x050000 QTextCodec* tc = QTextCodec::codecForLocale(); QByteArray* ba = new QByteArray(tc->fromUnicode(data)); #else QByteArray* ba = new QByteArray(data.toLatin1()); #endif ba->append('\0'); fh->rowData.append(ba); int dummy; Rev* c = new Rev(*ba, 0, idx, &dummy, !isMainHistory(fh)); return c; } const Rev* Git::fakeWorkDirRev(SCRef parent, SCRef log, SCRef longLog, int idx, FileHistory* fh) { QString patch; if (!isMainHistory(fh)) patch = getWorkDirDiff(fh->fileNames().first()); QString date(QString::number(QDateTime::currentDateTime().toTime_t())); QString author("-"); QStringList parents(parent); Rev* c = fakeRevData(ZERO_SHA, parents, author, date, log, longLog, patch, idx, fh); c->isDiffCache = true; c->lanes.append(EMPTY); return c; } const RevFile* Git::fakeWorkDirRevFile(const WorkingDirInfo& wd) { FileNamesLoader fl; RevFile* rf = new RevFile(); parseDiffFormat(*rf, wd.diffIndex, fl); rf->onlyModified = false; FOREACH_SL (it, wd.otherFiles) { appendFileName(*rf, *it, fl); rf->status.append(RevFile::UNKNOWN); rf->mergeParent.append(1); } RevFile cachedFiles; parseDiffFormat(cachedFiles, wd.diffIndexCached, fl); flushFileNames(fl); for (int i = 0; i < rf->count(); i++) if (findFileIndex(cachedFiles, filePath(*rf, i)) != -1) rf->status[i] |= RevFile::IN_INDEX; return rf; } void Git::getDiffIndex() { QString status; if (!run("git status", &status)) // git status refreshes the index, run as first return; QString head; if (!run("git rev-parse --revs-only HEAD", &head)) return; head = head.trimmed(); if (!head.isEmpty()) { // repository initialized but still no history if (!run("git diff-index " + head, &workingDirInfo.diffIndex)) return; // check for files already updated in cache, we will // save this information in status third field if (!run("git diff-index --cached " + head, &workingDirInfo.diffIndexCached)) return; } // get any file not in tree workingDirInfo.otherFiles = getOthersFiles(); // now mockup a RevFile revsFiles.insert(ZERO_SHA_RAW, fakeWorkDirRevFile(workingDirInfo)); // then mockup the corresponding Rev SCRef log = (isNothingToCommit() ? "Nothing to commit" : "Working directory changes"); const Rev* r = fakeWorkDirRev(head, log, status, revData->revOrder.count(), revData); revData->revs.insert(ZERO_SHA_RAW, r); revData->revOrder.append(ZERO_SHA_RAW); revData->earlyOutputCntBase = revData->revOrder.count(); // finally send it to GUI emit newRevsAdded(revData, revData->revOrder); } void Git::parseDiffFormatLine(RevFile& rf, SCRef line, int parNum, FileNamesLoader& fl) { if (line[1] == ':') { // it's a combined merge /* For combined merges rename/copy information is useless * because nor the original file name, nor similarity info * is given, just the status tracks that in the left/right * branch a renamed/copy occurred (as example status could * be RM or MR). For visualization purposes we could consider * the file as modified */ appendFileName(rf, line.section('\t', -1), fl); setStatus(rf, "M"); rf.mergeParent.append(parNum); } else { // faster parsing in normal case if (line.at(98) == '\t') { appendFileName(rf, line.mid(99), fl); setStatus(rf, line.at(97)); rf.mergeParent.append(parNum); } else // it's a rename or a copy, we are not in fast path now! setExtStatus(rf, line.mid(97), parNum, fl); } } //CT TODO can go in RevFile void Git::setStatus(RevFile& rf, SCRef rowSt) { char status = rowSt.at(0).toLatin1(); switch (status) { case 'M': case 'T': case 'U': rf.status.append(RevFile::MODIFIED); break; case 'D': rf.status.append(RevFile::DELETED); rf.onlyModified = false; break; case 'A': rf.status.append(RevFile::NEW); rf.onlyModified = false; break; case '?': rf.status.append(RevFile::UNKNOWN); rf.onlyModified = false; break; default: dbp("ASSERT in Git::setStatus, unknown status <%1>. " "'MODIFIED' will be used instead.", rowSt); rf.status.append(RevFile::MODIFIED); break; } } void Git::setExtStatus(RevFile& rf, SCRef rowSt, int parNum, FileNamesLoader& fl) { const QStringList sl(rowSt.split('\t', QString::SkipEmptyParts)); if (sl.count() != 3) { dbp("ASSERT in setExtStatus, unexpected status string %1", rowSt); return; } // we want store extra info with format "orig --> dest (Rxx%)" // but git give us something like "Rxx\t\t" SCRef type = sl[0]; SCRef orig = sl[1]; SCRef dest = sl[2]; const QString extStatusInfo(orig + " --> " + dest + " (" + type + "%)"); /* NOTE: we set rf.extStatus size equal to position of latest copied/renamed file. So it can have size lower then rf.count() if after copied/renamed file there are others. Here we have no possibility to know final dimension of this RefFile. We are still in parsing. */ // simulate new file appendFileName(rf, dest, fl); rf.mergeParent.append(parNum); rf.status.append(RevFile::NEW); rf.extStatus.resize(rf.status.size()); rf.extStatus[rf.status.size() - 1] = extStatusInfo; // simulate deleted orig file only in case of rename if (type.at(0) == 'R') { // renamed file appendFileName(rf, orig, fl); rf.mergeParent.append(parNum); rf.status.append(RevFile::DELETED); rf.extStatus.resize(rf.status.size()); rf.extStatus[rf.status.size() - 1] = extStatusInfo; } rf.onlyModified = false; } //CT TODO utility function; can go elsewhere void Git::parseDiffFormat(RevFile& rf, SCRef buf, FileNamesLoader& fl) { int parNum = 1, startPos = 0, endPos = buf.indexOf('\n'); while (endPos != -1) { SCRef line = buf.mid(startPos, endPos - startPos); if (line[0] == ':') // avoid sha's in merges output parseDiffFormatLine(rf, line, parNum, fl); else parNum++; startPos = endPos + 1; endPos = buf.indexOf('\n', endPos + 99); } } bool Git::startParseProc(SCList initCmd, FileHistory* fh, SCRef buf) { DataLoader* dl = new DataLoader(this, fh); // auto-deleted when done connect(this, SIGNAL(cancelLoading(const FileHistory*)), dl, SLOT(on_cancel(const FileHistory*))); connect(dl, SIGNAL(newDataReady(const FileHistory*)), this, SLOT(on_newDataReady(const FileHistory*))); connect(dl, SIGNAL(loaded(FileHistory*, ulong, int, bool, const QString&, const QString&)), this, SLOT(on_loaded(FileHistory*, ulong, int, bool, const QString&, const QString&))); return dl->start(initCmd, workDir, buf); } bool Git::startRevList(SCList args, FileHistory* fh) { QString baseCmd("git log --topo-order --no-color " #ifndef Q_OS_WIN32 "--log-size " // FIXME broken on Windows #endif "--parents --boundary -z " "--pretty=format:" GIT_LOG_FORMAT); // we don't need log message body for file history if (isMainHistory(fh)) baseCmd.append("%b"); QStringList initCmd(baseCmd.split(' ')); if (!isMainHistory(fh)) { /* NOTE: we don't use '--remove-empty' option because in case a file is deleted and then a new file with the same name is created again in the same directory then, with this option, file history is truncated to the file deletion revision. */ initCmd << QString("-r -m -p --full-index").split(' '); } else {} // initCmd << QString("--early-output"); currently disabled return startParseProc(initCmd + args, fh, QString()); } bool Git::startUnappliedList() { QStringList unAppliedShaList(getAllRefSha(UN_APPLIED)); if (unAppliedShaList.isEmpty()) return false; // WARNING: with this command 'git log' could send spurious // revs so we need some filter out logic during loading QString cmd("git log --no-color --parents -z " #ifndef Q_OS_WIN32 "--log-size " // FIXME broken on Windows #endif "--pretty=format:" GIT_LOG_FORMAT "%b ^HEAD"); QStringList sl(cmd.split(' ')); sl << unAppliedShaList; return startParseProc(sl, revData, QString()); } void Git::stop(bool saveCache) { // normally called when changing directory or closing EM_RAISE(exGitStopped); // stop all data sending from process and asks them // to terminate. Note that process could still keep // running for a while although silently emit cancelAllProcesses(); // non blocking // after cancelAllProcesses() procFinished() is not called anymore // TODO perhaps is better to call procFinished() also if process terminated // incorrectly as QProcess does. BUt first we need to fix FileView::on_loadCompleted() emit fileNamesLoad(1, revsFiles.count() - filesLoadingStartOfs); if (cacheNeedsUpdate && saveCache) { cacheNeedsUpdate = false; if (!filesLoadingCurSha.isEmpty()) // we are in the middle of a loading revsFiles.remove(toTempSha(filesLoadingCurSha)); // remove partial data if (!revsFiles.isEmpty()) { SHOW_MSG("Saving cache. Please wait..."); if (!Cache::save(gitDir, revsFiles, dirNamesVec, fileNamesVec)) dbs("ERROR unable to save file names cache"); } } } void Git::clearRevs() { revData->clear(); patchesStillToFind = 0; // TODO TEST WITH FILTERING firstNonStGitPatch = ""; workingDirInfo.clear(); revsFiles.remove(ZERO_SHA_RAW); } void Git::clearFileNames() { qDeleteAll(revsFiles); revsFiles.clear(); fileNamesMap.clear(); dirNamesMap.clear(); dirNamesVec.clear(); fileNamesVec.clear(); revsFilesShaBackupBuf.clear(); cacheNeedsUpdate = false; } bool Git::init(SCRef wd, bool askForRange, const QStringList* passedArgs, bool overwriteArgs, bool* quit) { // normally called when changing git directory. Must be called after stop() *quit = false; clearRevs(); /* we only update filtering info here, original arguments * are not overwritten. Only getArgs() can update arguments, * an exception is if flag overwriteArgs is set */ loadArguments.filteredLoading = (!overwriteArgs && passedArgs != NULL); if (loadArguments.filteredLoading) loadArguments.filterList = *passedArgs; if (overwriteArgs) // in this case must be passedArgs != NULL loadArguments.args = *passedArgs; try { setThrowOnStop(true); const QString msg1("Path is '" + workDir + "' Loading "); // check if repository is valid bool repoChanged; isGIT = getGitDBDir(wd, gitDir, repoChanged); if (repoChanged) { bool dummy; getBaseDir(wd, workDir, dummy); localDates.clear(); clearFileNames(); fileCacheAccessed = false; SHOW_MSG(msg1 + "file names cache..."); loadFileCache(); SHOW_MSG(""); } if (!isGIT) { setThrowOnStop(false); return false; } if (!passedArgs) { // update text codec according to repo settings bool dummy; QTextCodec::setCodecForLocale(getTextCodec(&dummy)); // load references SHOW_MSG(msg1 + "refs..."); if (!getRefs()) dbs("WARNING: no tags or heads found"); // startup input range dialog SHOW_MSG(""); if (startup || askForRange) { loadArguments.args = getArgs(quit, repoChanged); // must be called with refs loaded if (*quit) { setThrowOnStop(false); return false; } } // load StGit unapplied patches, must be after getRefs() if (isStGIT) { loadingUnAppliedPatches = startUnappliedList(); if (loadingUnAppliedPatches) { SHOW_MSG(msg1 + "StGIT unapplied patches..."); setThrowOnStop(false); // we will continue with init2() at // the end of loading... return true; } } } init2(); setThrowOnStop(false); return true; } catch (int i) { setThrowOnStop(false); if (isThrowOnStopRaised(i, "initializing 1")) { EM_THROW_PENDING; return false; } const QString info("Exception \'" + EM_DESC(i) + "\' " "not handled in init...re-throw"); dbs(info); throw; } } void Git::init2() { const QString msg1("Path is '" + workDir + "' Loading "); // after loading unapplied patch update base early output offset to // avoid losing unapplied patches at first early output event if (isStGIT) revData->earlyOutputCntBase = revData->revOrder.count(); try { setThrowOnStop(true); // load working directory files if (!loadArguments.filteredLoading && testFlag(DIFF_INDEX_F)) { SHOW_MSG(msg1 + "working directory changed files..."); getDiffIndex(); // blocking, we could be in setRepository() now } SHOW_MSG(msg1 + "revisions..."); // build up command line arguments QStringList args(loadArguments.args); if (loadArguments.filteredLoading) { if (!args.contains("--")) args << "--"; args << loadArguments.filterList; } if (!startRevList(args, revData)) SHOW_MSG("ERROR: unable to start 'git log'"); setThrowOnStop(false); } catch (int i) { setThrowOnStop(false); if (isThrowOnStopRaised(i, "initializing 2")) { EM_THROW_PENDING; return; } const QString info("Exception \'" + EM_DESC(i) + "\' " "not handled in init2...re-throw"); dbs(info); throw; } } void Git::on_newDataReady(const FileHistory* fh) { emit newRevsAdded(fh , fh->revOrder); } void Git::on_loaded(FileHistory* fh, ulong byteSize, int loadTime, bool normalExit, SCRef cmd, SCRef errorDesc) { if (!errorDesc.isEmpty()) { MainExecErrorEvent* e = new MainExecErrorEvent(cmd, errorDesc); QApplication::postEvent(parent(), e); } if (normalExit) { // do not send anything if killed on_newDataReady(fh); if (!loadingUnAppliedPatches) { fh->loadTime += loadTime; ulong kb = byteSize / 1024; double mbs = (double)byteSize / fh->loadTime / 1000; QString tmp; tmp.sprintf("Loaded %i revisions (%li KB), " "time elapsed: %i ms (%.2f MB/s)", fh->revs.count(), kb, fh->loadTime, mbs); if (!tryFollowRenames(fh)) emit loadCompleted(fh, tmp); if (isMainHistory(fh)) // wait the dust to settle down before to start // background file names loading for new revisions QTimer::singleShot(500, this, SLOT(loadFileNames())); } } if (loadingUnAppliedPatches) { loadingUnAppliedPatches = false; revData->lns->clear(); // again to reset lanes init2(); // continue with loading of remaining revisions } } bool Git::tryFollowRenames(FileHistory* fh) { if (isMainHistory(fh)) return false; QStringList oldNames; QMutableStringListIterator it(fh->renamedRevs); while (it.hasNext()) if (!populateRenamedPatches(it.next(), fh->curFNames, fh, &oldNames, false)) it.remove(); if (fh->renamedRevs.isEmpty()) return false; QStringList args; args << fh->renamedRevs << "--" << oldNames; fh->fNames << oldNames; fh->curFNames = oldNames; fh->renamedRevs.clear(); return startRevList(args, fh); } bool Git::populateRenamedPatches(SCRef renamedSha, SCList newNames, FileHistory* fh, QStringList* oldNames, bool backTrack) { QString runOutput; if (!run("git diff-tree -r -M " + renamedSha, &runOutput)) return false; // find the first renamed file with the new file name in renamedFiles list QString line; FOREACH_SL (it, newNames) { if (backTrack) { line = runOutput.section('\t' + *it + '\t', 0, 0, QString::SectionIncludeTrailingSep); line.chop(1); } else line = runOutput.section('\t' + *it + '\n', 0, 0); if (!line.isEmpty()) break; } if (line.contains('\n')) line = line.section('\n', -1, -1); SCRef status = line.section('\t', -2, -2).section(' ', -1, -1); if (!status.startsWith('R')) return false; if (backTrack) { SCRef nextFile = runOutput.section(line, 1, 1).section('\t', 1, 1); oldNames->append(nextFile.section('\n', 0, 0)); return true; } // get the diff betwen two files SCRef prevFileSha = line.section(' ', 2, 2); SCRef lastFileSha = line.section(' ', 3, 3); if (prevFileSha == lastFileSha) // just renamed runOutput.clear(); else if (!run("git diff --no-ext-diff -r --full-index " + prevFileSha + " " + lastFileSha, &runOutput)) return false; SCRef prevFile = line.section('\t', -1, -1); if (!oldNames->contains(prevFile)) oldNames->append(prevFile); // save the patch, will be used later to create a // proper graft sha with correct parent info if (fh) { QString tmp(!runOutput.isEmpty() ? runOutput : "diff --no-ext-diff --\nsimilarity index 100%\n"); fh->renamedPatches.insert(renamedSha, tmp); } return true; } void Git::populateFileNamesMap() { for (int i = 0; i < dirNamesVec.count(); ++i) dirNamesMap.insert(dirNamesVec[i], i); for (int i = 0; i < fileNamesVec.count(); ++i) fileNamesMap.insert(fileNamesVec[i], i); } void Git::loadFileCache() { if (!fileCacheAccessed) { fileCacheAccessed = true; QByteArray shaBuf; if (Cache::load(gitDir, revsFiles, dirNamesVec, fileNamesVec, shaBuf)) { revsFilesShaBackupBuf.append(shaBuf); populateFileNamesMap(); } else dbs("ERROR: unable to load file names cache"); } } void Git::loadFileNames() { indexTree(); // we are sure data loading is finished at this point int revCnt = 0; QString diffTreeBuf; FOREACH (ShaVect, it, revData->revOrder) { if (!revsFiles.contains(*it)) { const Rev* c = revLookup(*it); if (c->parentsCount() == 1) { // skip initials and merges diffTreeBuf.append(*it).append('\n'); revCnt++; } } } if (!diffTreeBuf.isEmpty()) { filesLoadingPending = filesLoadingCurSha = ""; filesLoadingStartOfs = revsFiles.count(); emit fileNamesLoad(3, revCnt); const QString runCmd("git diff-tree --no-color -r -C --stdin"); runAsync(runCmd, this, diffTreeBuf); } } bool Git::filterEarlyOutputRev(FileHistory* fh, Rev* rev) { if (fh->earlyOutputCnt < fh->revOrder.count()) { const ShaString& sha = fh->revOrder[fh->earlyOutputCnt++]; const Rev* c = revLookup(sha, fh); if (c) { if (rev->sha() != sha || rev->parents() != c->parents()) { // mismatch found! set correct value, 'rev' will // overwrite 'c' upon returning rev->orderIdx = c->orderIdx; revData->clear(false); // flush the tail } else return true; // filter out 'rev' } } // we have new revisions, exit from early output state fh->setEarlyOutputState(false); return false; } int Git::addChunk(FileHistory* fh, const QByteArray& ba, int start) { RevMap& r = fh->revs; int nextStart; Rev* rev; do { // only here we create a new rev rev = new Rev(ba, start, fh->revOrder.count(), &nextStart, !isMainHistory(fh)); if (nextStart == -2) { delete rev; fh->setEarlyOutputState(true); start = ba.indexOf('\n', start) + 1; } } while (nextStart == -2); if (nextStart == -1) { // half chunk detected delete rev; return -1; } const ShaString& sha = rev->sha(); if (fh->earlyOutputCnt != -1 && filterEarlyOutputRev(fh, rev)) { delete rev; return nextStart; } if (isStGIT) { if (loadingUnAppliedPatches) { // filter out possible spurious revs Reference* rf = lookupReference(sha); if (!(rf && (rf->type & UN_APPLIED))) { delete rev; return nextStart; } } // remove StGIT spurious revs filter if (!firstNonStGitPatch.isEmpty() && firstNonStGitPatch == sha) firstNonStGitPatch = ""; // StGIT called with --all option creates spurious revs so filter // out unknown revs until no more StGIT patches are waited and // firstNonStGitPatch is reached if (!(firstNonStGitPatch.isEmpty() && patchesStillToFind == 0) && !loadingUnAppliedPatches && isMainHistory(fh)) { Reference* rf = lookupReference(sha); if (!(rf && (rf->type & APPLIED))) { delete rev; return nextStart; } } if (r.contains(sha)) { // StGIT unapplied patches could be sent again by // 'git log' as example if called with --all option. if (r[sha]->isUnApplied) { delete rev; return nextStart; } // could be a side effect of 'git log -m', see below if (isMainHistory(fh) || rev->parentsCount() < 2) dbp("ASSERT: addChunk sha <%1> already received", sha); } } if (r.isEmpty() && !isMainHistory(fh)) { bool added = copyDiffIndex(fh, sha); rev->orderIdx = added ? 1 : 0; } if ( !isMainHistory(fh) && !fh->renamedPatches.isEmpty() && fh->renamedPatches.contains(sha)) { // this is the new rev with renamed file, the rev is correct but // the patch, create a new rev with proper patch and use that instead const Rev* prevSha = revLookup(sha, fh); Rev* c = fakeRevData(sha, rev->parents(), rev->author(), rev->authorDate(), rev->shortLog(), rev->longLog(), fh->renamedPatches[sha], prevSha->orderIdx, fh); r.insert(sha, c); // overwrite old content fh->renamedPatches.remove(sha); return nextStart; } if (!isMainHistory(fh) && rev->parentsCount() > 1 && r.contains(sha)) { /* In this case git log is called with -m option and merges are splitted in one commit per parent but all them have the same sha. So we add only the first to fh->revOrder to display history correctly, but we nevertheless add all the commits to 'r' so that annotation code can get the patches. */ QString mergeSha; int i = 0; do mergeSha = QString::number(++i) + " m " + sha; while (r.contains(toTempSha(mergeSha))); const ShaString& ss = toPersistentSha(mergeSha, shaBackupBuf); r.insert(ss, rev); } else { r.insert(sha, rev); fh->revOrder.append(sha); if (rev->parentsCount() == 0 && !isMainHistory(fh)) fh->renamedRevs.append(sha); } if (isStGIT) { // updateLanes() is called too late, after loadingUnAppliedPatches // has been reset so update the lanes now. if (loadingUnAppliedPatches) { Rev* c = const_cast(revLookup(sha, fh)); c->isUnApplied = true; c->lanes.append(UNAPPLIED); } else if (patchesStillToFind > 0 || !isMainHistory(fh)) { // try to avoid costly lookup Reference* rf = lookupReference(sha); if (rf && (rf->type & APPLIED)) { Rev* c = const_cast(revLookup(sha, fh)); c->isApplied = true; if (isMainHistory(fh)) { patchesStillToFind--; if (patchesStillToFind == 0) // any rev will be discarded until // firstNonStGitPatch arrives firstNonStGitPatch = c->parent(0); } } } } return nextStart; } bool Git::copyDiffIndex(FileHistory* fh, SCRef parent) { // must be called with empty revs and empty revOrder if (!fh->revOrder.isEmpty() || !fh->revs.isEmpty()) { dbs("ASSERT in copyDiffIndex: called with wrong context"); return false; } const Rev* r = revLookup(ZERO_SHA); if (!r) return false; const RevFile* files = getFiles(ZERO_SHA); if (!files || findFileIndex(*files, fh->fileNames().first()) == -1) return false; // insert a custom ZERO_SHA rev with proper parent const Rev* rf = fakeWorkDirRev(parent, "Working directory changes", "long log\n", 0, fh); fh->revs.insert(ZERO_SHA_RAW, rf); fh->revOrder.append(ZERO_SHA_RAW); return true; } void Git::setLane(SCRef sha, FileHistory* fh) { Lanes* l = fh->lns; uint i = fh->firstFreeLane; QVector ba; const ShaString& ss = toPersistentSha(sha, ba); const ShaVect& shaVec(fh->revOrder); for (uint cnt = shaVec.count(); i < cnt; ++i) { const ShaString& curSha = shaVec[i]; Rev* r = const_cast(revLookup(curSha, fh)); if (r->lanes.count() == 0) updateLanes(*r, *l, curSha); if (curSha == ss) break; } fh->firstFreeLane = ++i; } void Git::updateLanes(Rev& c, Lanes& lns, SCRef sha) { // we could get third argument from c.sha(), but we are in fast path here // and c.sha() involves a deep copy, so we accept a little redundancy if (lns.isEmpty()) lns.init(sha); bool isDiscontinuity; bool isFork = lns.isFork(sha, isDiscontinuity); bool isMerge = (c.parentsCount() > 1); bool isInitial = (c.parentsCount() == 0); if (isDiscontinuity) lns.changeActiveLane(sha); // uses previous isBoundary state lns.setBoundary(c.isBoundary()); // update must be here if (isFork) lns.setFork(sha); if (isMerge) lns.setMerge(c.parents()); if (c.isApplied) lns.setApplied(); if (isInitial) lns.setInitial(); lns.getLanes(c.lanes); // here lanes are snapshotted SCRef nextSha = (isInitial) ? "" : QString(c.parent(0)); lns.nextParent(nextSha); if (c.isApplied) lns.afterApplied(); if (isMerge) lns.afterMerge(); if (isFork) lns.afterFork(); if (lns.isBranch()) lns.afterBranch(); // QString tmp = "", tmp2; // for (uint i = 0; i < c.lanes.count(); i++) { // tmp2.setNum(c.lanes[i]); // tmp.append(tmp2 + "-"); // } // qDebug("%s %s", tmp.toUtf8().data(), sha.toUtf8().data()); } void Git::procFinished() { flushFileNames(fileLoader); filesLoadingPending = filesLoadingCurSha = ""; emit fileNamesLoad(1, revsFiles.count() - filesLoadingStartOfs); } void Git::procReadyRead(const QByteArray& fileChunk) { QTextCodec* tc = QTextCodec::codecForLocale(); if (filesLoadingPending.isEmpty()) filesLoadingPending = tc->toUnicode(fileChunk); else filesLoadingPending.append(tc->toUnicode(fileChunk)); // add to previous half lines RevFile* rf = NULL; if (!filesLoadingCurSha.isEmpty() && revsFiles.contains(toTempSha(filesLoadingCurSha))) rf = const_cast(revsFiles[toTempSha(filesLoadingCurSha)]); int nextEOL = filesLoadingPending.indexOf('\n'); int lastEOL = -1; while (nextEOL != -1) { SCRef line(filesLoadingPending.mid(lastEOL + 1, nextEOL - lastEOL - 1)); if (line.at(0) != ':') { SCRef sha = line.left(40); if (!rf || sha != filesLoadingCurSha) { // new commit rf = new RevFile(); revsFiles.insert(toPersistentSha(sha, revsFilesShaBackupBuf), rf); filesLoadingCurSha = sha; cacheNeedsUpdate = true; } else dbp("ASSERT: repeated sha %1 in file names loading", sha); } else // line.constref(0) == ':' parseDiffFormatLine(*rf, line, 1, fileLoader); lastEOL = nextEOL; nextEOL = filesLoadingPending.indexOf('\n', lastEOL + 1); } if (lastEOL != -1) filesLoadingPending.remove(0, lastEOL + 1); emit fileNamesLoad(2, revsFiles.count() - filesLoadingStartOfs); } void Git::flushFileNames(FileNamesLoader& fl) { if (!fl.rf) return; QByteArray& b = fl.rf->pathsIdx; QVector& dirs = fl.rfDirs; b.clear(); b.resize(2 * dirs.size() * static_cast(sizeof(int))); int* d = (int*)(b.data()); for (int i = 0; i < dirs.size(); i++) { d[i] = dirs.at(i); d[dirs.size() + i] = fl.rfNames.at(i); } dirs.clear(); fl.rfNames.clear(); fl.rf = NULL; } void Git::appendFileName(RevFile& rf, SCRef name, FileNamesLoader& fl) { if (fl.rf != &rf) { flushFileNames(fl); fl.rf = &rf; } int idx = name.lastIndexOf('/') + 1; SCRef dr = name.left(idx); SCRef nm = name.mid(idx); QHash::const_iterator it(dirNamesMap.constFind(dr)); if (it == dirNamesMap.constEnd()) { int idx = dirNamesVec.count(); dirNamesMap.insert(dr, idx); dirNamesVec.append(dr); fl.rfDirs.append(idx); } else fl.rfDirs.append(*it); it = fileNamesMap.constFind(nm); if (it == fileNamesMap.constEnd()) { int idx = fileNamesVec.count(); fileNamesMap.insert(nm, idx); fileNamesVec.append(nm); fl.rfNames.append(idx); } else fl.rfNames.append(*it); } void Git::updateDescMap(const Rev* r,uint idx, QHash, bool>& dm, QHash >& dv) { QVector descVec; if (r->descRefsMaster != -1) { const Rev* tmp = revLookup(revData->revOrder[r->descRefsMaster]); const QVector& nr = tmp->descRefs; for (int i = 0; i < nr.count(); i++) { if (!dv.contains(nr[i])) { dbp("ASSERT descendant for %1 not found", r->sha()); return; } const QVector& dvv = dv[nr[i]]; // copy the whole vector instead of each element // in the first iteration of the loop below descVec = dvv; // quick (shared) copy for (int y = 0; y < dvv.count(); y++) { uint v = (uint)dvv[y]; QPair key = qMakePair(idx, v); QPair keyN = qMakePair(v, idx); dm.insert(key, true); dm.insert(keyN, false); // we don't want duplicated entry, otherwise 'dvv' grows // greatly in repos with many tagged development branches if (i > 0 && !descVec.contains(v)) // i > 0 is rare, no descVec.append(v); // need to optimize } } } descVec.append(idx); dv.insert(idx, descVec); } void Git::mergeBranches(Rev* p, const Rev* r) { int r_descBrnMaster = (checkRef(r->sha(), BRANCH | RMT_BRANCH) ? r->orderIdx : r->descBrnMaster); if (p->descBrnMaster == r_descBrnMaster || r_descBrnMaster == -1) return; // we want all the descendant branches, so just avoid duplicates const QVector& src1 = revLookup(revData->revOrder[p->descBrnMaster])->descBranches; const QVector& src2 = revLookup(revData->revOrder[r_descBrnMaster])->descBranches; QVector dst(src1); for (int i = 0; i < src2.count(); i++) if (qFind(src1.constBegin(), src1.constEnd(), src2[i]) == src1.constEnd()) dst.append(src2[i]); p->descBranches = dst; p->descBrnMaster = p->orderIdx; } void Git::mergeNearTags(bool down, Rev* p, const Rev* r, const QHash, bool>& dm) { bool isTag = checkRef(r->sha(), TAG); int r_descRefsMaster = isTag ? r->orderIdx : r->descRefsMaster; int r_ancRefsMaster = isTag ? r->orderIdx : r->ancRefsMaster; if (down && (p->descRefsMaster == r_descRefsMaster || r_descRefsMaster == -1)) return; if (!down && (p->ancRefsMaster == r_ancRefsMaster || r_ancRefsMaster == -1)) return; // we want the nearest tag only, so remove any tag // that is ancestor of any other tag in p U r const ShaVect& ro = revData->revOrder; const ShaString& sha1 = down ? ro[p->descRefsMaster] : ro[p->ancRefsMaster]; const ShaString& sha2 = down ? ro[r_descRefsMaster] : ro[r_ancRefsMaster]; const QVector& src1 = down ? revLookup(sha1)->descRefs : revLookup(sha1)->ancRefs; const QVector& src2 = down ? revLookup(sha2)->descRefs : revLookup(sha2)->ancRefs; QVector dst(src1); for (int s2 = 0; s2 < src2.count(); s2++) { bool add = false; for (int s1 = 0; s1 < src1.count(); s1++) { if (src2[s2] == src1[s1]) { add = false; break; } QPair key = qMakePair((uint)src2[s2], (uint)src1[s1]); if (!dm.contains(key)) { // could be empty if all tags are independent add = true; // could be an independent path continue; } add = (down && dm[key]) || (!down && !dm[key]); if (add) dst[s1] = -1; // mark for removing else break; } if (add) dst.append(src2[s2]); } QVector& nearRefs = (down ? p->descRefs : p->ancRefs); int& nearRefsMaster = (down ? p->descRefsMaster : p->ancRefsMaster); nearRefs.clear(); for (int s2 = 0; s2 < dst.count(); s2++) if (dst[s2] != -1) nearRefs.append(dst[s2]); nearRefsMaster = p->orderIdx; } void Git::indexTree() { const ShaVect& ro = revData->revOrder; if (ro.count() == 0) return; // we keep the pairs(x, y). Value is true if x is // ancestor of y or false if y is ancestor of x QHash, bool> descMap; QHash > descVect; // walk down the tree from latest to oldest, // compute children and nearest descendants for (uint i = 0, cnt = ro.count(); i < cnt; i++) { uint type = checkRef(ro[i]); bool isB = (type & (BRANCH | RMT_BRANCH)); bool isT = (type & TAG); const Rev* r = revLookup(ro[i]); if (isB) { Rev* rr = const_cast(r); if (r->descBrnMaster != -1) { const ShaString& sha = ro[r->descBrnMaster]; rr->descBranches = revLookup(sha)->descBranches; } rr->descBranches.append(i); } if (isT) { updateDescMap(r, i, descMap, descVect); Rev* rr = const_cast(r); rr->descRefs.clear(); rr->descRefs.append(i); } for (uint y = 0; y < r->parentsCount(); y++) { Rev* p = const_cast(revLookup(r->parent(y))); if (p) { p->children.append(i); if (p->descBrnMaster == -1) p->descBrnMaster = isB ? r->orderIdx : r->descBrnMaster; else mergeBranches(p, r); if (p->descRefsMaster == -1) p->descRefsMaster = isT ? r->orderIdx : r->descRefsMaster; else mergeNearTags(optGoDown, p, r, descMap); } } } // walk backward through the tree and compute nearest tagged ancestors for (int i = ro.count() - 1; i >= 0; i--) { const Rev* r = revLookup(ro[i]); bool isTag = checkRef(ro[i], TAG); if (isTag) { Rev* rr = const_cast(r); rr->ancRefs.clear(); rr->ancRefs.append(i); } for (int y = 0; y < r->children.count(); y++) { Rev* c = const_cast(revLookup(ro[r->children[y]])); if (c) { if (c->ancRefsMaster == -1) c->ancRefsMaster = isTag ? r->orderIdx:r->ancRefsMaster; else mergeNearTags(!optGoDown, c, r, descMap); } } } } qgit-2.7/src/git.h000066400000000000000000000261341305655150700140660ustar00rootroot00000000000000/* Author: Marco Costalba (C) 2005-2007 Copyright: See COPYING file that comes with this distribution */ #ifndef GIT_H #define GIT_H #include "exceptionmanager.h" #include "common.h" template struct QPair; class QRegExp; class QTextCodec; class Annotate; //class DataLoader; class Domain; class FileHistory; class Lanes; class MyProcess; class Git : public QObject { Q_OBJECT public: explicit Git(QObject* parent); // used as self-documenting boolean parameters static const bool optSaveCache = true; static const bool optGoDown = true; //CT TODO private, enum static const bool optOnlyLoaded = true; static const bool optDragDrop = true; static const bool optFold = true; static const bool optAmend = true; //CT TODO enum static const bool optOnlyInIndex = true; //CT TODO private, enum enum RefType { TAG = 1, BRANCH = 2, RMT_BRANCH = 4, CUR_BRANCH = 8, REF = 16, APPLIED = 32, UN_APPLIED = 64, ANY_REF = 127 }; struct TreeEntry { TreeEntry(SCRef n, SCRef s, SCRef t) : name(n), sha(s), type(t) {} bool operator<(const TreeEntry&) const; QString name; QString sha; QString type; }; typedef QList TreeInfo; void setDefaultModel(FileHistory* fh) { revData = fh; } void checkEnvironment(); void userInfo(SList info); const QStringList getGitConfigList(bool global); bool getGitDBDir(SCRef wd, QString& gd, bool& changed); bool getBaseDir(SCRef wd, QString& bd, bool& changed); bool init(SCRef wd, bool range, const QStringList* args, bool overwrite, bool* quit); void stop(bool saveCache); void setThrowOnStop(bool b); bool isThrowOnStopRaised(int excpId, SCRef curContext); void setLane(SCRef sha, FileHistory* fh); Annotate* startAnnotate(FileHistory* fh, QObject* guiObj); const FileAnnotation* lookupAnnotation(Annotate* ann, SCRef sha); void cancelAnnotate(Annotate* ann); bool startFileHistory(SCRef sha, SCRef startingFileName, FileHistory* fh); void cancelDataLoading(const FileHistory* fh); void cancelProcess(MyProcess* p); bool isCommittingMerge() const { return isMergeHead; } bool isStGITStack() const { return isStGIT; } bool isPatchName(SCRef nm); bool isSameFiles(SCRef tree1Sha, SCRef tree2Sha); static bool isImageFile(SCRef file); static bool isBinaryFile(SCRef file); bool isNothingToCommit(); bool isUnknownFiles() const { return (workingDirInfo.otherFiles.count() > 0); } bool isTextHighlighter() const { return isTextHighlighterFound; } const QString textHighlighterVersion() const { return textHighlighterVersionFound; } bool isMainHistory(const FileHistory* fh) { return (fh == revData); } MyProcess* getDiff(SCRef sha, QObject* receiver, SCRef diffToSha, bool combined); const QString getWorkDirDiff(SCRef fileName = ""); MyProcess* getFile(SCRef fileSha, QObject* receiver, QByteArray* result, SCRef fileName); MyProcess* getHighlightedFile(SCRef fileSha, QObject* receiver, QString* result, SCRef fileName); const QString getFileSha(SCRef file, SCRef revSha); bool saveFile(SCRef fileSha, SCRef fileName, SCRef path); void getFileFilter(SCRef path, ShaSet& shaSet) const; bool getPatchFilter(SCRef exp, bool isRegExp, ShaSet& shaSet); const RevFile* getFiles(SCRef sha, SCRef sha2 = "", bool all = false, SCRef path = ""); bool getTree(SCRef ts, TreeInfo& ti, bool wd, SCRef treePath); static const QString getLocalDate(SCRef gitDate); const QString getCurrentBranchName() const {return curBranchName;} const QString getDesc(SCRef sha, QRegExp& slogRE, QRegExp& lLogRE, bool showH, FileHistory* fh); const QString getLastCommitMsg(); const QString getNewCommitMsg(); const QString getLaneParent(SCRef fromSHA, int laneNum); const QStringList getChildren(SCRef parent); const QStringList getNearTags(bool goDown, SCRef sha); const QStringList getDescendantBranches(SCRef sha, bool shaOnly = false); const QString getShortLog(SCRef sha); const QString getTagMsg(SCRef sha); const Rev* revLookup(const ShaString& sha, const FileHistory* fh = NULL) const; const Rev* revLookup(SCRef sha, const FileHistory* fh = NULL) const; uint checkRef(const ShaString& sha, uint mask = ANY_REF) const; uint checkRef(SCRef sha, uint mask = ANY_REF) const; const QString getRevInfo(SCRef sha); const QString getRefSha(SCRef refName, RefType type = ANY_REF, bool askGit = true); const QStringList getRefName(SCRef sha, RefType type) const; const QStringList getAllRefNames(uint mask, bool onlyLoaded); const QStringList getAllRefSha(uint mask); const QStringList sortShaListByIndex(SCList shaList); void getWorkDirFiles(SList files, SList dirs, RevFile::StatusFlag status); QTextCodec* getTextCodec(bool* isGitArchive); bool formatPatch(SCList shaList, SCRef dirPath, SCRef remoteDir = ""); bool updateIndex(SCList selFiles); bool commitFiles(SCList files, SCRef msg, bool amend); bool applyPatchFile(SCRef patchPath, bool fold, bool sign); bool resetCommits(int parentDepth); bool stgCommit(SCList selFiles, SCRef msg, SCRef patchName, bool fold); bool stgPush(SCRef sha); bool stgPop(SCRef sha); void setTextCodec(QTextCodec* tc); void addExtraFileInfo(QString* rowName, SCRef sha, SCRef diffToSha, bool allMergeFiles); void removeExtraFileInfo(QString* rowName); void formatPatchFileHeader(QString* rowName, SCRef sha, SCRef dts, bool cmb, bool all); int findFileIndex(const RevFile& rf, SCRef name); const QString filePath(const RevFile& rf, uint i) const { return dirNamesVec[rf.dirAt(i)] + fileNamesVec[rf.nameAt(i)]; } void setCurContext(Domain* d) { curDomain = d; } Domain* curContext() const { return curDomain; } signals: void newRevsAdded(const FileHistory*, const QVector&); void loadCompleted(const FileHistory*, const QString&); void cancelLoading(const FileHistory*); void cancelAllProcesses(); void annotateReady(Annotate*, bool, const QString&); void fileNamesLoad(int, int); void changeFont(const QFont&); public slots: void procReadyRead(const QByteArray&); void procFinished(); private slots: void loadFileCache(); void loadFileNames(); void on_runAsScript_eof(); void on_getHighlightedFile_eof(); void on_newDataReady(const FileHistory*); void on_loaded(FileHistory*, ulong,int,bool,const QString&,const QString&); private: friend class MainImpl; friend class DataLoader; friend class ConsoleImpl; friend class RevsView; struct Reference { // stores tag information associated to a revision Reference() : type(0) {} uint type; QStringList branches; QStringList remoteBranches; QStringList tags; QStringList refs; QString tagObj; // TODO support more then one obj QString tagMsg; QString stgitPatch; }; typedef QHash RefMap; struct WorkingDirInfo { void clear() { diffIndex = diffIndexCached = ""; otherFiles.clear(); } QString diffIndex; QString diffIndexCached; QStringList otherFiles; }; WorkingDirInfo workingDirInfo; struct LoadArguments { // used to pass arguments to init2() QStringList args; bool filteredLoading; QStringList filterList; }; LoadArguments loadArguments; struct FileNamesLoader { FileNamesLoader() : rf(NULL) {} RevFile* rf; QVector rfDirs; QVector rfNames; }; FileNamesLoader fileLoader; void init2(); bool run(SCRef cmd, QString* out = NULL, QObject* rcv = NULL, SCRef buf = ""); bool run(QByteArray* runOutput, SCRef cmd, QObject* rcv = NULL, SCRef buf = ""); MyProcess* runAsync(SCRef cmd, QObject* rcv, SCRef buf = ""); MyProcess* runAsScript(SCRef cmd, QObject* rcv = NULL, SCRef buf = ""); const QStringList getArgs(bool* quit, bool repoChanged); bool getRefs(); void parseStGitPatches(SCList patchNames, SCList patchShas); void clearRevs(); void clearFileNames(); bool startRevList(SCList args, FileHistory* fh); bool startUnappliedList(); bool startParseProc(SCList initCmd, FileHistory* fh, SCRef buf); bool tryFollowRenames(FileHistory* fh); bool populateRenamedPatches(SCRef sha, SCList nn, FileHistory* fh, QStringList* on, bool bt); bool filterEarlyOutputRev(FileHistory* fh, Rev* rev); int addChunk(FileHistory* fh, const QByteArray& ba, int ofs); void parseDiffFormat(RevFile& rf, SCRef buf, FileNamesLoader& fl); void parseDiffFormatLine(RevFile& rf, SCRef line, int parNum, FileNamesLoader& fl); void getDiffIndex(); Rev* fakeRevData(SCRef sha, SCList parents, SCRef author, SCRef date, SCRef log, SCRef longLog, SCRef patch, int idx, FileHistory* fh); const Rev* fakeWorkDirRev(SCRef parent, SCRef log, SCRef longLog, int idx, FileHistory* fh); const RevFile* fakeWorkDirRevFile(const WorkingDirInfo& wd); bool copyDiffIndex(FileHistory* fh, SCRef parent); const RevFile* insertNewFiles(SCRef sha, SCRef data); const RevFile* getAllMergeFiles(const Rev* r); bool runDiffTreeWithRenameDetection(SCRef runCmd, QString* runOutput); bool isParentOf(SCRef par, SCRef child); bool isTreeModified(SCRef sha); void indexTree(); void updateDescMap(const Rev* r, uint i, QHash,bool>& dm, QHash >& dv); void mergeNearTags(bool down, Rev* p, const Rev* r, const QHash, bool>&dm); void mergeBranches(Rev* p, const Rev* r); void updateLanes(Rev& c, Lanes& lns, SCRef sha); bool mkPatchFromWorkDir(SCRef msg, SCRef patchFile, SCList files); const QStringList getOthersFiles(); const QStringList getOtherFiles(SCList selFiles, bool onlyInIndex); const QString getNewestFileName(SCList args, SCRef fileName); static const QString colorMatch(SCRef txt, QRegExp& regExp); void appendFileName(RevFile& rf, SCRef name, FileNamesLoader& fl); void flushFileNames(FileNamesLoader& fl); void populateFileNamesMap(); const QString formatList(SCList sl, SCRef name, bool inOneLine = true); static const QString quote(SCRef nm); static const QString quote(SCList sl); static const QStringList noSpaceSepHack(SCRef cmd); void removeDeleted(SCList selFiles); void setStatus(RevFile& rf, SCRef rowSt); void setExtStatus(RevFile& rf, SCRef rowSt, int parNum, FileNamesLoader& fl); void appendNamesWithId(QStringList& names, SCRef sha, SCList data, bool onlyLoaded); Reference* lookupReference(const ShaString& sha); Reference* lookupOrAddReference(const ShaString& sha); EM_DECLARE(exGitStopped); Domain* curDomain; QString workDir; // workDir is always without trailing '/' QString gitDir; QString filesLoadingPending; QString filesLoadingCurSha; QString curBranchName; int filesLoadingStartOfs; bool cacheNeedsUpdate; bool errorReportingEnabled; bool isMergeHead; bool isStGIT; bool isGIT; bool isTextHighlighterFound; QString textHighlighterVersionFound; bool loadingUnAppliedPatches; bool fileCacheAccessed; int patchesStillToFind; QString firstNonStGitPatch; RevFileMap revsFiles; QVector revsFilesShaBackupBuf; RefMap refsShaMap; QVector shaBackupBuf; StrVect fileNamesVec; StrVect dirNamesVec; QHash fileNamesMap; // quick lookup file name QHash dirNamesMap; // quick lookup directory name FileHistory* revData; }; #endif qgit-2.7/src/help.h000066400000000000000000000522061305655150700142320ustar00rootroot00000000000000/* Help content is generated automatically from README by helpgen script */ static const char* helpInfo = "\n" "

QGit Handbook

\n" "

Command line arguments

\n" "

Run qgit from a git working directory, command line arguments\n" "are filtered by git log. Some examples:

\n" "
qgit --no-merges\n"
"qgit v2.6.18.. include/scsi drivers/scsi\n"
"qgit --since=\"2 weeks ago\" -- kernel/\n"
"qgit -r --name-status release..test
\n" "

If qgit is launched without arguments or if you change archive with\n" "open menu, a dialog for range select is shown.\n" "You can select top and bottom rev tags from the list or paste a\n" "specific revision. Values are passed to git log to narrow\n" "data loading to chosen revisions.

\n" "
\n" "

Main view

\n" "

You can navigate through logs, file names, file history, archive tree.\n" "All the views will be updated accordingly.

\n" "

Copy/paste is supported on all fields. Copy (CTRL+C) is supported on\n" "all views.

\n" "

All the references found recursively under .git/refs/ directory are\n" "highlighted according to their type: current branch(HEAD), branch, tag,\n" "other. Reference names and any associated messages can be viewed in status\n" "bar when a tagged revision is selected.

\n" "

When you right click on main view a context sensitive pop-up menu is shows\n" "available commands and a quick jump tag list.

\n" "
\n" "
\n" "Key bindings\n" "
\n" "
\n" "
\n" "
\n" "\n" "\n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" "\n" "
\n" " r\n" " \n" " Go to revisions list page\n" "
\n" " p\n" " \n" " Go to patch page\n" "
\n" " f\n" " \n" " Go to file page\n" "
\n" " <Alt+wheel>\n" " \n" " Go to next/previous page\n" "
\n" " t\n" " \n" " Toggle tree view\n" "
\n" " s\n" " \n" " Toggle view of secondary panes\n" "
\n" " h\n" " \n" " Toggle view of revision header\n" "
\n" " <Home>\n" " \n" " Move to first revision\n" "
\n" " <End>\n" " \n" " Move to last revision\n" "
\n" " i\n" " \n" " Move up one revision in main view (global scope)\n" "
\n" " n, k\n" " \n" " Move down one revision in main view (global scope)\n" "
\n" " <Shift-Up>\n" " \n" " Move to previous highlighted line\n" "
\n" " <Shift-Down>\n" " \n" " Move to next highlighted line\n" "
\n" " <Left>\n" " \n" " Go back in history list\n" "
\n" " <Right>\n" " \n" " Go forward in history list\n" "
\n" " <CTRL-plus>\n" " \n" " Increase font size\n" "
\n" " <CTRL-minus>\n" " \n" " Decrease font size\n" "
\n" " <Delete>, b\n" " \n" " Scroll content up one page\n" "
\n" " <Backspace>\n" " \n" " Scroll content up one page\n" "
\n" " <Space>\n" " \n" " Scroll content down one page\n" "
\n" " u\n" " \n" " Scroll content up 18 lines\n" "
\n" " d\n" " \n" " Scroll content down 18 lines\n" "
\n" "
\n" "
\n" "Directory tree\n" "
\n" "
\n" "

\n" "From menu or toolbar button it is possible to show a side panel with\n" "tree view of repository files and directories.\n" "

\n" "

Double clicking on a file opens file annotation window. With filter by tree\n" "button it is possible to compress revision list to show only selected\n" "files/directories in tree view.

\n" "

Tree view supports multi-selection. When you right click on a file on tree\n" "view a context sensitive pop-up menu is shows with available commands.

\n" "
\n" "
\n" "Working directory changes\n" "
\n" "
\n" "

\n" "When Check working dir flag is set, as example from main view context\n" "pop-up menu, a pseudo-revision is shown and highlighted at the top of the\n" "list. Highlight and revision name reflect current working directory status:\n" "Nothing to commit or Working directory changes respectively.\n" "

\n" "

To check for working directory modified files set corresponding preference in\n" "Edit→Settings→Working dir. QGit checks for possible new files added in\n" "working directory using ignoring rules according to git ls-files\n" "specifications, see menu Edit→Settings→Working dir.

\n" "\n" "\n" "

Tip

If you don’t need to see modified files in working dir, disable\n" "corresponding setting and start-up time will be shorter.
\n" "
\n" "
\n" "Lane info\n" "
\n" "
\n" "

\n" "Selecting a lane with mouse right button will display a pop-up\n" "with the list of children and parent. Select one and you jump to it.\n" "

\n" "
\n" "
\n" "Filter / Highlight\n" "
\n" "
\n" "

\n" "Use the combo box to select where you want to filter or highlight on.\n" "Currently supported fields are: log header, log message, revision author,\n" "revision SHA1, file name and patch content.\n" "

\n" "

Write a filter string, press filter button and the view\n" "will update showing only commits that contain the filter string,\n" "case insensitive. Toggle filter button to release the filter.

\n" "

Alternatively press the magnifying glass button, in this case matched\n" "lines will be highlighted, you can use <Shift-Up> and <Shift-Down>\n" "keys to browse them. Toggle the button to remove the highlighting.

\n" "\n" "\n" "

Note

In case of patch content regexp filtering, the given string is\n" "interpreted as a POSIX regular expression, not as a simple substring.
\n" "\n" "\n" "

Tip

Very useful to quick retrieve a sha writing only first 3-4\n" "digits and filtering / highlighting on revision sha. The sha value\n" "can then be copied from SHA field.
\n" "\n" "\n" "

Tip

It is possible to insert an abbreviated sha directly in the\n" "SHA line edit at the top right of the window. After pressing enter\n" "this will trigger an higlighting of the matched revisions. It is\n" "a kind of shortcut of the previous tip.
\n" "
\n" "
\n" "Save patch series\n" "
\n" "
\n" "

\n" "After mouse selecting the chosen revisions (use standard CTRL+left click)\n" "for single select or SHIFT+left click for range select), press Save\n" "Patch button or use file menu and a dialog will let you choose patches\n" "destination directory. Then git format-patch-script will be called and\n" "patches created. It is possible to specify additional options with\n" "Edit→Settings menu.\n" "

\n" "
\n" "
\n" "Apply patch\n" "
\n" "
\n" "

\n" "This menu entry is complementary to save patch and it’s an interface\n" "to git am.\n" "

\n" "
\n" "
\n" "Drag and drop\n" "
\n" "
\n" "

\n" "It is possible to drag some selected revs from one instance of qgit to another\n" "open on a different archive. In this case git format-patch is used in the\n" "dragging archive to create temporary patches imported in the dropping archive\n" "by git am.\n" "

\n" "
\n" "
\n" "Make branch\n" "
\n" "
\n" "

\n" "Select a revision and open Edit→Make Branch or use right click context\n" "pop-up menu. A dialog will be shown asking for a branch name.\n" "

\n" "
\n" "
\n" "Make tag\n" "
\n" "
\n" "

\n" "Select a revision and open Edit→Make Tag or use right click context\n" "pop-up menu. Two dialogs will be shown, the first asking for a tag name, the\n" "second for a tag message (not mandatory). If a non empty message is written,\n" "this will be saved together with the tag. Tags and tag messages can be viewed\n" "in status bar when a tagged revision is selected.\n" "

\n" "
\n" "
\n" "Delete tag\n" "
\n" "
\n" "

\n" "Select a tagged revision and open Edit→Delete Tag or use right click\n" "context pop-up menu. After confirmation the selected revision will be\n" "untagged.\n" "

\n" "
\n" "
\n" "Save file\n" "
\n" "
\n" "

\n" "Select a file from tree or file list and open File→Save file as or use the\n" "tree view context sensitive pop-up menu (right click), a dialog will be shown\n" "asking for a file name (default to current) and destination directory. Input\n" "a valid name, press OK and the file will be saved.\n" "

\n" "
\n" "
\n" "Commit changes\n" "
\n" "
\n" "

\n" "When enabled with Edit→Settings→Working dirDiff against working dir\n" "and there is something committable, a special highlighted first revision is\n" "shown, with the status of the archive and the possible pending stuff.\n" "From Edit→Commit it is then possible to invoke the commit dialog.\n" "

\n" "

In commit dialog select the files to commit or, simply, to sync with index\n" "(call git update-index on them). A proper commit message may be entered and,\n" "after confirmation, changes are committed and a new revision is created.

\n" "

It is also possible to amend last commit. The Edit→Amend commit opens the\n" "same dialog, but changes are added to the head commit instead of creating new\n" "commit.

\n" "

The core commit function is performed by git commit.

\n" "\n" "\n" "

Tip

It is possible to use a template for commit message, use\n" "Edit→Settings→Commit to define template file path.
\n" "
\n" "
\n" "
\n" "

Patch viewer

\n" "

To open patch tab use context menu, double click on a revision or file in\n" "main view or select View→View patch menu (CTRL+P). The patch shown is\n" "the diff of current selected commit against:

\n" "
    \n" "
  • \n" "

    \n" "Parent (default)\n" "

    \n" "
  • \n" "
  • \n" "

    \n" "HEAD\n" "

    \n" "
  • \n" "
  • \n" "

    \n" "Selected SHA or reference name\n" "

    \n" "
  • \n" "
\n" "

In the last case SHA is chosen by writing or pasting a tree-ish or a reference\n" "names in the corresponding field and pressing return. You get the same result\n" "also with a CTRL+right click on a revision in main list. Selected target\n" "will be highlighted. CTRL+right click again on the highlighted revision to\n" "release the filter.

\n" "

With the filter button at the right of the tool bar it is possible to\n" "toggle the display of removed code lines. This can be useful to easy\n" "reading of the patch.

\n" "
\n" "

External diff tool

\n" "

From View→External diff it is possible to invoke an external diff tool,\n" "as example to view the diffs in a two vertical tiled windows.

\n" "

External diff tool shows the diffs between two files.\n" "First file is the current selected file of current revision.\n" "Second file is the same file of the parent revision or of a specific revision\n" "if diff to sha feature is enabled (diff target is highlighted, see above).

\n" "

Default external viewer is kompare, but it is possible to set a preferred one\n" "from Edit→Settings→External Diff Tool.

\n" "
\n" "

File viewer

\n" "

It is possible to view file contents of any file at any revision time in\n" "history.

\n" "
\n" "
\n" "File list panel\n" "
\n" "
\n" "

\n" "In the bottom right of main view a list of files modified by current\n" "revision is shown. Selecting a file name will update the patch view\n" "to center on the file. File names colors use the following convention\n" "

\n" "
    \n" "
  • \n" "

    \n" "black for modified files\n" "

    \n" "
  • \n" "
  • \n" "

    \n" "green for new files\n" "

    \n" "
  • \n" "
  • \n" "

    \n" "red for removed files\n" "

    \n" "
  • \n" "
  • \n" "

    \n" "dark blue for renamed/copied files\n" "

    \n" "
  • \n" "
\n" "
\n" "
\n" "Merge files\n" "
\n" "
\n" "

\n" "In case of merges the groups of files corresponding to each merge parent\n" "are separated by two empty lines.\n" "

\n" "

In case of merges you can chose between to see all the merge files or only\n" "the interesting ones (default), i.e. the files modified by more then one\n" "merge parent.

\n" "
\n" "
\n" "File content\n" "
\n" "
\n" "

\n" "To view file content double click on a file name in tree view, or use context\n" "menu in file list or select View→View file menu (CTRL+A).\n" "

\n" "

In file view page will be shown current revision’s file content and file\n" "history.

\n" "

It is possible to copy to the clipboard the selected content with CTRL+C or\n" "with the corresponding button.

\n" "
\n" "
\n" "File annotations\n" "
\n" "
\n" "

\n" "On opening or updating file viewer, file history will be retrieved from archive\n" "together with file content. Annotations are then calculated in background\n" "and the view is updated when ready.\n" "

\n" "

Double clicking on an annotation index number will update history list\n" "to point to corresponding revision.

\n" "

Hovering the mouse over an annotation index will show a tool tip with the\n" "corresponding revision description.

\n" "

File content will change too, to show new selected revision file. To keep\n" "the same view content after double clicking, probably what you want, just pin\n" "it with Pin view check button. Next to the check button there is a spinbox\n" "to show/select the current revision number.

\n" "

Double click on history list entry to update main, patch and tree views to\n" "corresponding revision.

\n" "
\n" "
\n" "Code region filter\n" "
\n" "
\n" "

\n" "When annotation info is available the filter button is enabled and it is\n" "possible to mouse select a region of file content. Then, when pressing\n" "the filter button, only revisions that modify the selected region will be\n" "visible. Selected code region is highlighted and a shrunken history is\n" "shown. Filter button is a toggle button, so just press it again to\n" "release the filter.\n" "

\n" "
\n" "
\n" "Syntax highlighter\n" "
\n" "
\n" "

\n" "If GNU Source-highlight (http://www.gnu.org/software/src-highlite/) is\n" "installed and in PATH then it is possible to toggle source code highlight\n" "pressing the Color text tool button. Please refer to Source-highlight\n" "site for the list of supported languages and additional documentation.\n" "

\n" "
\n" "
\n" "
\n" "

Actions

\n" "

Actions can be added/removed using a dedicated dialog invoked\n" "from Actions→Setup actions… menu. Actions can be activated\n" "clicking on their name from the Actions menu.

\n" "

Each action can be associated to a list of any type of git or shell\n" "commands or to an external script.

\n" "

While an action is running a terminal window is shown to display the\n" "corresponding output.

\n" "

An action can also ask for command line arguments before to run so\n" "to allow for maximum flexibility.

\n" "\n" "\n" "

Note

command line arguments are always appended to the first command only.\n" "This lets you define an action like:
\n" "
git fetch\n"
"git merge
\n" "

And if you type origin when prompted, the action executed will be:

\n" "
git fetch origin\n"
"git merge
\n" "

If you need a more complex arguments passing with a shell like notation\n" "define a script and associate your action to it.

\n" "
\n" "

Integration with StGIT

\n" "

When a StGIT stack is found on top of a git archive, qgit transparently\n" "handles the added information.

\n" "

Integration with StGIT is implemented both by new and modified functions.

\n" "

New functions are automatically activated:

    \n" "
  • \n" "

    \n" "Visualization of applied and unapplied patches in main view.\n" "

    \n" "
  • \n" "
  • \n" "

    \n" "Interface to push/pop patches by a mouse right click on selected items.\n" " Push supports also multi-selection.\n" "

    \n" "
  • \n" "
\n" "

Existing functions change behavior:

    \n" "
  • \n" "

    \n" "Amend commit dialog refreshes top stack patch with modified files instead\n" " of amending the commit. It is appropriately renamed in the menu.\n" "

    \n" "
  • \n" "
  • \n" "

    \n" "Commit dialog creates a new patch on the top of the stack filled with\n" " modified working directory content instead of commit a new revision to\n" " git repository.\n" "

    \n" "
  • \n" "
  • \n" "

    \n" "Apply patch changes to interface StGIT import and fold commands instead\n" " of applying patch directly on the git repository.\n" "

    \n" "
  • \n" "
\n" "
\n"; qgit-2.7/src/help.ui000066400000000000000000000056431305655150700144230ustar00rootroot00000000000000 HelpBase 0 0 631 451 Help :/icons/resources/help.png 0 0 0 5 Qt::Vertical false true 0 6 Qt::Horizontal QSizePolicy::Expanding 20 20 80 0 O&K Alt+K true true buttonOk clicked() HelpBase close() 20 20 20 20 qgit-2.7/src/helpgen.sh000077500000000000000000000010601305655150700151020ustar00rootroot00000000000000#!/bin/sh # Generate help.h from README set -e infile="$1" outfile="$2" tmpfile1="$2.t1" tmpfile2="$2.t2" asciidoc --attribute 'newline=\n' --backend=html4 --no-header-footer \ --out-file="$tmpfile1" "$infile" sed -n -e 's/"/\\"/g' -e '/Command line arguments/,$s/^.*$/"&\\n"/p' \ "$tmpfile1" >"$tmpfile2" cat >"$outfile" <

QGit Handbook

\n" `cat "$tmpfile2"` "\n"; EOF rm -f "$tmpfile1" "$tmpfile2" qgit-2.7/src/icons.qrc000066400000000000000000000040311305655150700147440ustar00rootroot00000000000000 resources/1downarrow.png resources/1uparrow.png resources/bookmark.png resources/bookmark_add.png resources/cancel.png resources/colorize.png resources/configure.png resources/editcopy.png resources/encrypted.png resources/filter.png resources/find.png resources/folder.png resources/folder_open.png resources/help.png resources/html.png resources/image.png resources/mail_get.png resources/mail_send.png resources/minusonly.png resources/misc.png resources/next.png resources/ok.png resources/openterm.png resources/pencil.png resources/plusminus.png resources/plusonly.png resources/previous.png resources/qgit.png resources/range_select.png resources/reload.png resources/remove.png resources/shellscript.png resources/source_c.png resources/source_cpp.png resources/source_h.png resources/source_java.png resources/source_pl.png resources/source_py.png resources/tab_remove.png resources/tar.png resources/txt.png resources/vcs_commit.png resources/view_choose.png resources/view_top_bottom.png resources/view_tree.png resources/wizard.png qgit-2.7/src/inputdialog.cpp000066400000000000000000000151251305655150700161530ustar00rootroot00000000000000#include "inputdialog.h" #include "common.h" #include #include #include #include #include #include #include #include #include #include namespace QGit { InputDialog::WidgetItem::WidgetItem() : widget(NULL) { } void InputDialog::WidgetItem::init(QWidget* w, const char *name) { widget = w; prop_name = name; } QString parseString(const QString &value, const InputDialog::VariableMap &vars) { if (value.startsWith('$')) return vars.value(value.mid(1), QString()).toString(); else return value; } QStringList parseStringList(const QString &value, const InputDialog::VariableMap &vars) { QStringList values = value.split(','); QStringList result; for (QStringList::iterator it=values.begin(), end=values.end(); it!=end; ++it) { if (it->startsWith('$')) result.append(vars.value(value.mid(1), QStringList()).toStringList()); else result.append(*it); } return result; } class RefNameValidator : public QValidator { public: RefNameValidator(bool allowEmpty=false, QObject *parent=0) : QValidator(parent) , invalid("[ ~^:\?*[]") , allowEmpty(allowEmpty) {} void fixup(QString& input) const; State validate(QString & input, int & pos) const; private: const QRegExp invalid; bool allowEmpty; }; void RefNameValidator::fixup(QString &input) const { // remove invalid chars input.replace(invalid, ""); input.replace("/.","/"); // no dot after slash input.replace("..","."); // no two dots in a row input.replace("//","/"); // no two slashes in a row input.replace("@{", "@"); // no sequence @{ } QValidator::State RefNameValidator::validate(QString &input, int &pos) const { // https://www.kernel.org/pub/software/scm/git/docs/git-check-ref-format.html // automatically remove invalid chars QString front = input.left(pos); fixup(front); QString rear = input.mid(pos); fixup(rear); input = front + rear; // keep cursor were it was pos = front.length(); QString fixed(input); fixup(fixed); if (fixed != input) return Invalid; // empty string or single @ are not allowed if ((input.isEmpty() && !allowEmpty) || input == "@") return Intermediate; return Acceptable; } InputDialog::InputDialog(const QString &cmd, const VariableMap &variables, const QString &title, QWidget *parent, Qt::WindowFlags f) : QDialog(parent, f) , cmd(cmd) { this->setWindowTitle(title); QGridLayout *layout = new QGridLayout(this); QRegExp re("%(([a-z_]+)([[]([a-z ,]+)[]])?:)?([^%=]+)(=[^%]+)?%"); int start = 0; int row = 0; while ((start = re.indexIn(cmd, start)) != -1) { const QString type = re.cap(2); const QStringList opts = re.cap(4).split(',', QString::SkipEmptyParts); const QString name = re.cap(5); const QString value = re.cap(6).mid(1); if (widgets.count(name)) { // widget already created if (!type.isEmpty()) dbs("token must not be redefined: " + name); continue; } WidgetItemPtr item (new WidgetItem()); item->start = start; item->end = start = start + re.matchedLength(); if (type == "combobox") { QComboBox *w = new QComboBox(this); w->addItems(parseStringList(value, variables)); if (opts.contains("editable")) w->setEditable(true); w->setMinimumWidth(100); if (opts.contains("ref")) { w->setValidator(new RefNameValidator(opts.contains("empty"))); validators.insert(name, w->validator()); connect(w, SIGNAL(editTextChanged(QString)), this, SLOT(validate())); } item->init(w, "currentText"); } else if (type == "listbox") { QListView *w = new QListView(this); w->setModel(new QStringListModel(parseStringList(value, variables))); item->init(w, NULL); } else if (type == "lineedit" || type == "") { QLineEdit *w = new QLineEdit(this); w->setText(parseString(value, variables)); QStringList values = parseStringList(value, variables); if (!values.isEmpty()) // use default string list as w->setCompleter(new QCompleter(values)); if (opts.contains("ref")) { w->setValidator(new RefNameValidator(opts.contains("empty"))); validators.insert(name, w->validator()); connect(w, SIGNAL(textEdited(QString)), this, SLOT(validate())); } item->init(w, "text"); } else if (type == "textedit") { QTextEdit *w = new QTextEdit(this); w->setText(parseString(value, variables)); item->init(w, "plainText"); } else { dbs("unknown widget type: " + type); continue; } widgets.insert(name, item); if (name.startsWith('_')) { // _name triggers hiding of label layout->addWidget(item->widget, row, 1); } else { layout->addWidget(new QLabel(name + ":"), row, 0); layout->addWidget(item->widget, row, 1); } ++row; } QDialogButtonBox *buttons = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel); layout->addWidget(buttons, row, 0, 1, 2); okButton = buttons->button(QDialogButtonBox::Ok); connect(okButton, SIGNAL(pressed()), this, SLOT(accept())); connect(buttons->button(QDialogButtonBox::Cancel), SIGNAL(pressed()), this, SLOT(reject())); validate(); } QWidget *InputDialog::widget(const QString &token) { WidgetItemPtr item = widgets.value(token); return item ? item->widget : NULL; } QVariant InputDialog::value(const QString &token) const { WidgetItemPtr item = widgets.value(token); if (!item) { dbs("unknown token: " + token); return QString(); } return item->widget->property(item->prop_name); } bool InputDialog::validate() { bool result=true; for (QMap::const_iterator it=validators.begin(), end=validators.end(); result && it != end; ++it) { QString val = value(it.key()).toString(); int pos=0; if (it.value()->validate(val, pos) != QValidator::Acceptable) result=false; } okButton->setEnabled(result); return result; } QString InputDialog::replace(const VariableMap &variables) const { QString result = cmd; for (WidgetMap::const_iterator it = widgets.begin(), end = widgets.end(); it != end; ++it) { QString token = "%" + it.key() + "%"; WidgetItemPtr item = it.value(); QString value = item->widget->property(item->prop_name).toString(); result.replace(item->start, item->end - item->start, value); // replace main token result.replace(token, value); // replace all other occurences of %name% } for (VariableMap::const_iterator it=variables.begin(), end=variables.end(); it != end; ++it) { QString token = "$" + it.key(); QString val = it.value().type() == QVariant::StringList ? it.value().toStringList().join(" ") : it.value().toString(); result.replace(token, val); } return result; } } // namespace QGit qgit-2.7/src/inputdialog.h000066400000000000000000000030751305655150700156210ustar00rootroot00000000000000#pragma once #include #include class QValidator; class QPushButton; namespace QGit { /** create an input dialog from a command containing tokens of the form * %[options]:=% * For default values, variables of the form $VAR_NAME can be used. * For each of those tokens, an input widget is created. * Supported widgets include: lineedit, combobox, textedit * options include: * - [ref]: enable ref name validation * - [editable]: ediable combobox */ class InputDialog : public QDialog { Q_OBJECT struct WidgetItem { WidgetItem(); void init(QWidget* w, const char *name); const char *prop_name; // property name QWidget *widget; int start, end; }; typedef QSharedPointer WidgetItemPtr; typedef QMap WidgetMap; // map from token names to WidgetMap widgets; QString cmd; QMap validators; QPushButton *okButton; public: typedef QMap VariableMap; explicit InputDialog(const QString &cmd, const VariableMap &variables, const QString &title="", QWidget *parent = 0, Qt::WindowFlags f = 0); /// any widgets defined? bool empty() const {return widgets.empty();} /// retrieve widget of given token QWidget *widget(const QString &token); /// retrieve value of given token QVariant value(const QString &token) const; /// replace all tokens in cmd by their values QString replace(const VariableMap &variables) const; public Q_SLOTS: virtual bool validate(); }; } // namespace QGit qgit-2.7/src/lanes.cpp000066400000000000000000000130651305655150700147370ustar00rootroot00000000000000/* Description: history graph computation Author: Marco Costalba (C) 2005-2007 Copyright: See COPYING file that comes with this distribution */ #include #include "common.h" #include "lanes.h" #define IS_NODE(x) (x == NODE || x == NODE_R || x == NODE_L) using namespace QGit; void Lanes::init(const QString& expectedSha) { clear(); activeLane = 0; setBoundary(false); add(BRANCH, expectedSha, activeLane); } void Lanes::clear() { typeVec.clear(); nextShaVec.clear(); } void Lanes::setBoundary(bool b) { // changes the state so must be called as first one NODE = b ? BOUNDARY_C : MERGE_FORK; NODE_R = b ? BOUNDARY_R : MERGE_FORK_R; NODE_L = b ? BOUNDARY_L : MERGE_FORK_L; boundary = b; if (boundary) typeVec[activeLane] = BOUNDARY; } bool Lanes::isFork(const QString& sha, bool& isDiscontinuity) { int pos = findNextSha(sha, 0); isDiscontinuity = (activeLane != pos); if (pos == -1) // new branch case return false; return (findNextSha(sha, pos + 1) != -1); /* int cnt = 0; while (pos != -1) { cnt++; pos = findNextSha(sha, pos + 1); // if (isDiscontinuity) // isDiscontinuity = (activeLane != pos); } return (cnt > 1); */ } void Lanes::setFork(const QString& sha) { int rangeStart, rangeEnd, idx; rangeStart = rangeEnd = idx = findNextSha(sha, 0); while (idx != -1) { rangeEnd = idx; typeVec[idx] = TAIL; idx = findNextSha(sha, idx + 1); } typeVec[activeLane] = NODE; int& startT = typeVec[rangeStart]; int& endT = typeVec[rangeEnd]; if (startT == NODE) startT = NODE_L; if (endT == NODE) endT = NODE_R; if (startT == TAIL) startT = TAIL_L; if (endT == TAIL) endT = TAIL_R; for (int i = rangeStart + 1; i < rangeEnd; i++) { int& t = typeVec[i]; if (t == NOT_ACTIVE) t = CROSS; else if (t == EMPTY) t = CROSS_EMPTY; } } void Lanes::setMerge(const QStringList& parents) { // setFork() must be called before setMerge() if (boundary) return; // handle as a simple active line int& t = typeVec[activeLane]; bool wasFork = (t == NODE); bool wasFork_L = (t == NODE_L); bool wasFork_R = (t == NODE_R); bool startJoinWasACross = false, endJoinWasACross = false; t = NODE; int rangeStart = activeLane, rangeEnd = activeLane; QStringList::const_iterator it(parents.constBegin()); for (++it; it != parents.constEnd(); ++it) { // skip first parent int idx = findNextSha(*it, 0); if (idx != -1) { if (idx > rangeEnd) { rangeEnd = idx; endJoinWasACross = typeVec[idx] == CROSS; } if (idx < rangeStart) { rangeStart = idx; startJoinWasACross = typeVec[idx] == CROSS; } typeVec[idx] = JOIN; } else rangeEnd = add(HEAD, *it, rangeEnd + 1); } int& startT = typeVec[rangeStart]; int& endT = typeVec[rangeEnd]; if (startT == NODE && !wasFork && !wasFork_R) startT = NODE_L; if (endT == NODE && !wasFork && !wasFork_L) endT = NODE_R; if (startT == JOIN && !startJoinWasACross) startT = JOIN_L; if (endT == JOIN && !endJoinWasACross) endT = JOIN_R; if (startT == HEAD) startT = HEAD_L; if (endT == HEAD) endT = HEAD_R; for (int i = rangeStart + 1; i < rangeEnd; i++) { int& t = typeVec[i]; if (t == NOT_ACTIVE) t = CROSS; else if (t == EMPTY) t = CROSS_EMPTY; else if (t == TAIL_R || t == TAIL_L) t = TAIL; } } void Lanes::setInitial() { int& t = typeVec[activeLane]; if (!IS_NODE(t) && t != APPLIED) t = (boundary ? BOUNDARY : INITIAL); } void Lanes::setApplied() { // applied patches are not merges, nor forks typeVec[activeLane] = APPLIED; // TODO test with boundaries } void Lanes::changeActiveLane(const QString& sha) { int& t = typeVec[activeLane]; if (t == INITIAL || isBoundary(t)) t = EMPTY; else t = NOT_ACTIVE; int idx = findNextSha(sha, 0); // find first sha if (idx != -1) typeVec[idx] = ACTIVE; // called before setBoundary() else idx = add(BRANCH, sha, activeLane); // new branch activeLane = idx; } void Lanes::afterMerge() { if (boundary) return; // will be reset by changeActiveLane() for (int i = 0; i < typeVec.count(); i++) { int& t = typeVec[i]; if (isHead(t) || isJoin(t) || t == CROSS) t = NOT_ACTIVE; else if (t == CROSS_EMPTY) t = EMPTY; else if (IS_NODE(t)) t = ACTIVE; } } void Lanes::afterFork() { for (int i = 0; i < typeVec.count(); i++) { int& t = typeVec[i]; if (t == CROSS) t = NOT_ACTIVE; else if (isTail(t) || t == CROSS_EMPTY) t = EMPTY; if (!boundary && IS_NODE(t)) t = ACTIVE; // boundary will be reset by changeActiveLane() } while (typeVec.last() == EMPTY) { typeVec.pop_back(); nextShaVec.pop_back(); } } bool Lanes::isBranch() { return (typeVec[activeLane] == BRANCH); } void Lanes::afterBranch() { typeVec[activeLane] = ACTIVE; // TODO test with boundaries } void Lanes::afterApplied() { typeVec[activeLane] = ACTIVE; // TODO test with boundaries } void Lanes::nextParent(const QString& sha) { nextShaVec[activeLane] = (boundary ? "" : sha); } int Lanes::findNextSha(const QString& next, int pos) { for (int i = pos; i < nextShaVec.count(); i++) if (nextShaVec[i] == next) return i; return -1; } int Lanes::findType(int type, int pos) { for (int i = pos; i < typeVec.count(); i++) if (typeVec[i] == type) return i; return -1; } int Lanes::add(int type, const QString& next, int pos) { // first check empty lanes starting from pos if (pos < (int)typeVec.count()) { pos = findType(EMPTY, pos); if (pos != -1) { typeVec[pos] = type; nextShaVec[pos] = next; return pos; } } // if all lanes are occupied add a new lane typeVec.append(type); nextShaVec.append(next); return typeVec.count() - 1; } qgit-2.7/src/lanes.h000066400000000000000000000034341305655150700144030ustar00rootroot00000000000000/* Author: Marco Costalba (C) 2005-2007 Copyright: See COPYING file that comes with this distribution */ #ifndef LANES_H #define LANES_H #include #include class QStringList; // // At any given time, the Lanes class represents a single revision (row) of the history graph. // The Lanes class contains a vector of the sha1 hashes of the next commit to appear in each lane (column). // The Lanes class also contains a vector used to decide which glyph to draw on the history graph. // // For each revision (row) (from recent (top) to ancient past (bottom)), the Lanes class is updated, and the // current revision (row) of glyphs is saved elsewhere (via getLanes()). // // The ListView class is responsible for rendering the glyphs. // class Lanes { public: Lanes() {} // init() will setup us later, when data is available bool isEmpty() { return typeVec.empty(); } void init(const QString& expectedSha); void clear(); bool isFork(const QString& sha, bool& isDiscontinuity); void setBoundary(bool isBoundary); void setFork(const QString& sha); void setMerge(const QStringList& parents); void setInitial(); void setApplied(); void changeActiveLane(const QString& sha); void afterMerge(); void afterFork(); bool isBranch(); void afterBranch(); void afterApplied(); void nextParent(const QString& sha); void getLanes(QVector &ln) { ln = typeVec; } // O(1) vector is implicitly shared private: int findNextSha(const QString& next, int pos); int findType(int type, int pos); int add(int type, const QString& next, int pos); int activeLane; QVector typeVec; // Describes which glyphs should be drawn. QVector nextShaVec; // The sha1 hashes of the next commit to appear in each lane (column). bool boundary; int NODE, NODE_L, NODE_R; }; #endif qgit-2.7/src/listview.cpp000066400000000000000000000614361305655150700155100ustar00rootroot00000000000000/* Description: qgit revision list view Author: Marco Costalba (C) 2005-2007 Copyright: See COPYING file that comes with this distribution */ #include #include #include #include #include #include #include #include "FileHistory.h" #include "domain.h" #include "git.h" #include "listview.h" using namespace QGit; ListView::ListView(QWidget* parent) : QTreeView(parent), d(NULL), git(NULL), fh(NULL), lp(NULL) {} void ListView::setup(Domain* dm, Git* g) { d = dm; git = g; fh = d->model(); st = &(d->st); filterNextContextMenuRequest = false; setFont(QGit::STD_FONT); // create ListViewProxy unplugged, will be plug // to the model only when filtering is needed lp = new ListViewProxy(this, d, git); setModel(fh); ListViewDelegate* lvd = new ListViewDelegate(git, lp, this); lvd->setLaneHeight(fontMetrics().height() + 2); setItemDelegate(lvd); setupGeometry(); // after setting delegate // shortcuts are activated only if widget is visible, this is good new QShortcut(Qt::Key_Up, this, SLOT(on_keyUp())); new QShortcut(Qt::Key_Down, this, SLOT(on_keyDown())); connect(lvd, SIGNAL(updateView()), viewport(), SLOT(update())); connect(this, SIGNAL(diffTargetChanged(int)), lvd, SLOT(diffTargetChanged(int))); connect(this, SIGNAL(customContextMenuRequested(const QPoint&)), this, SLOT(on_customContextMenuRequested(const QPoint&))); } ListView::~ListView() { git->cancelDataLoading(fh); // non blocking } const QString ListView::sha(int row) const { if (!lp->sourceModel()) // unplugged return fh->sha(row); QModelIndex idx = lp->mapToSource(lp->index(row, 0)); return fh->sha(idx.row()); } int ListView::row(SCRef sha) const { if (!lp->sourceModel()) // unplugged return fh->row(sha); int row = fh->row(sha); QModelIndex idx = fh->index(row, 0); return lp->mapFromSource(idx).row(); } void ListView::setupGeometry() { QPalette pl = palette(); pl.setColor(QPalette::Base, ODD_LINE_COL); pl.setColor(QPalette::AlternateBase, EVEN_LINE_COL); setPalette(pl); // does not seem to inherit application paletteAnnotate QHeaderView* hv = header(); hv->setStretchLastSection(true); #if QT_VERSION >= 0x050000 hv->setSectionResizeMode(LOG_COL, QHeaderView::Interactive); hv->setSectionResizeMode(TIME_COL, QHeaderView::Interactive); hv->setSectionResizeMode(ANN_ID_COL, QHeaderView::ResizeToContents); #else hv->setResizeMode(LOG_COL, QHeaderView::Interactive); hv->setResizeMode(TIME_COL, QHeaderView::Interactive); hv->setResizeMode(ANN_ID_COL, QHeaderView::ResizeToContents); #endif hv->resizeSection(GRAPH_COL, DEF_GRAPH_COL_WIDTH); hv->resizeSection(LOG_COL, DEF_LOG_COL_WIDTH); hv->resizeSection(AUTH_COL, DEF_AUTH_COL_WIDTH); hv->resizeSection(TIME_COL, DEF_TIME_COL_WIDTH); if (git->isMainHistory(fh)) hideColumn(ANN_ID_COL); } void ListView::scrollToNextHighlighted(int direction) { // Depending on the value of direction, scroll to: // -1 = the next highlighted item above the current one (i.e. newer in history) // 1 = the next highlighted item below the current one (i.e. older in history) // 0 = the first highlighted item from the top of the list QModelIndex idx = currentIndex(); if (!direction) { idx = idx.sibling(0,0); if (lp->isHighlighted(idx.row())) { setCurrentIndex(idx); return; } } do { idx = (direction >= 0 ? indexBelow(idx) : indexAbove(idx)); if (!idx.isValid()) return; } while (!lp->isHighlighted(idx.row())); setCurrentIndex(idx); } void ListView::scrollToCurrent(ScrollHint hint) { if (currentIndex().isValid()) scrollTo(currentIndex(), hint); } void ListView::on_keyUp() { QModelIndex idx = indexAbove(currentIndex()); if (idx.isValid()) setCurrentIndex(idx); } void ListView::on_keyDown() { QModelIndex idx = indexBelow(currentIndex()); if (idx.isValid()) setCurrentIndex(idx); } void ListView::on_changeFont(const QFont& f) { setFont(f); ListViewDelegate* lvd = static_cast(itemDelegate()); lvd->setLaneHeight(fontMetrics().height()); scrollToCurrent(); } const QString ListView::currentText(int column) { QModelIndex idx = model()->index(currentIndex().row(), column); return (idx.isValid() ? idx.data().toString() : ""); } int ListView::getLaneType(SCRef sha, int pos) const { const Rev* r = git->revLookup(sha, fh); return (r && pos < r->lanes.count() && pos >= 0 ? r->lanes.at(pos) : -1); } void ListView::showIdValues() { fh->setAnnIdValid(); viewport()->update(); } void ListView::getSelectedItems(QStringList& selectedItems) { selectedItems.clear(); QModelIndexList ml = selectionModel()->selectedRows(); FOREACH (QModelIndexList, it, ml) selectedItems.append(sha((*it).row())); // selectedRows() returns the items in an unspecified order, // so be sure rows are ordered from newest to oldest. selectedItems = git->sortShaListByIndex(selectedItems); } const QString ListView::shaFromAnnId(uint id) { if (git->isMainHistory(fh)) return ""; return sha(model()->rowCount() - id); } int ListView::filterRows(bool isOn, bool highlight, SCRef filter, int colNum, ShaSet* set) { setUpdatesEnabled(false); int matchedNum = lp->setFilter(isOn, highlight, filter, colNum, set); viewport()->update(); setUpdatesEnabled(true); UPDATE_DOMAIN(d); return matchedNum; } bool ListView::update() { int stRow = row(st->sha()); if (stRow == -1) return false; // main/tree view asked us a sha not in history QModelIndex index = currentIndex(); QItemSelectionModel* sel = selectionModel(); if (index.isValid() && (index.row() == stRow)) { if (sel->isSelected(index) != st->selectItem()) sel->select(index, QItemSelectionModel::Toggle); scrollTo(index); } else { // setCurrentIndex() does not clear previous // selections in a multi selection QListView clearSelection(); QModelIndex newIndex = model()->index(stRow, 0); if (newIndex.isValid()) { // emits QItemSelectionModel::currentChanged() setCurrentIndex(newIndex); scrollTo(newIndex); if (!st->selectItem()) sel->select(newIndex, QItemSelectionModel::Deselect); } } if (git->isMainHistory(fh)) emit diffTargetChanged(row(st->diffToSha())); return currentIndex().isValid(); } void ListView::currentChanged(const QModelIndex& index, const QModelIndex&) { SCRef selRev = sha(index.row()); if (st->sha() != selRev) { // to avoid looping st->setSha(selRev); st->setFileName(""); st->setSelectItem(true); UPDATE_DOMAIN(d); } } void ListView::markDiffToSha(SCRef sha) { if (sha != st->diffToSha()) { st->setDiffToSha(sha); emit showStatusMessage("Marked " + sha + " for diff. (Ctrl-RightClick)"); } else { st->setDiffToSha(""); // restore std view emit showStatusMessage("Unmarked diff reference."); } UPDATE_DOMAIN(d); } bool ListView::filterRightButtonPressed(QMouseEvent* e) { QModelIndex index = indexAt(e->pos()); SCRef selSha = sha(index.row()); if (selSha.isEmpty()) return false; if (e->modifiers() == Qt::ControlModifier) { // check for 'diff to' function if (selSha != ZERO_SHA) { filterNextContextMenuRequest = true; markDiffToSha(selSha); return true; // filter event out } } // check for 'children & parents' function, i.e. if mouse is on the graph if (index.column() == GRAPH_COL) { filterNextContextMenuRequest = true; QStringList parents, children; if (getLaneParentsChildren(selSha, e->pos().x(), parents, children)) emit lanesContextMenuRequested(parents, children); return true; // filter event out } return false; } void ListView::mousePressEvent(QMouseEvent* e) { lastRefName = refNameAt(e->pos()); if (currentIndex().isValid() && e->button() == Qt::LeftButton) d->setReadyToDrag(true); if (e->button() == Qt::RightButton && filterRightButtonPressed(e)) return; // filtered out QTreeView::mousePressEvent(e); } void ListView::mouseReleaseEvent(QMouseEvent* e) { d->setReadyToDrag(false); // in case of just click without moving lastRefName = ""; // reset QTreeView::mouseReleaseEvent(e); } void ListView::mouseMoveEvent(QMouseEvent* e) { if (d->isReadyToDrag()) { if (indexAt(e->pos()).row() == currentIndex().row()) return; // move at least by one line to activate drag if (!d->setDragging(true)) return; QStringList selRevs; getSelectedItems(selRevs); selRevs.removeAll(ZERO_SHA); if (!selRevs.empty()) emit revisionsDragged(selRevs); // blocking until drop event d->setDragging(false); } QTreeView::mouseMoveEvent(e); } void ListView::dragEnterEvent(QDragEnterEvent* e) { if (e->mimeData()->hasFormat("text/plain")) e->accept(); } void ListView::dragMoveEvent(QDragMoveEvent* e) { // already checked by dragEnterEvent() e->accept(); } void ListView::dropEvent(QDropEvent *e) { SCList remoteRevs(e->mimeData()->text().split('\n', QString::SkipEmptyParts)); if (!remoteRevs.isEmpty()) { // some sanity check on dropped data SCRef sha(remoteRevs.first().section('@', 0, 0)); SCRef remoteRepo(remoteRevs.first().section('@', 1)); if (sha.length() == 40 && !remoteRepo.isEmpty()) emit revisionsDropped(remoteRevs); } } void ListView::on_customContextMenuRequested(const QPoint& pos) { QModelIndex index = indexAt(pos); if (!index.isValid()) return; if (filterNextContextMenuRequest) { // event filter does not work on them filterNextContextMenuRequest = false; return; } emit contextMenu(sha(index.row()), POPUP_LIST_EV); } bool ListView::getLaneParentsChildren(SCRef sha, int x, SList p, SList c) { ListViewDelegate* lvd = static_cast(itemDelegate()); uint lane = x / lvd->laneWidth(); int t = getLaneType(sha, lane); if (t == EMPTY || t == -1) return false; // first find the parents p.clear(); QString root; if (!isFreeLane(t)) { p = git->revLookup(sha, fh)->parents(); // pointer cannot be NULL root = sha; } else { SCRef par(git->getLaneParent(sha, lane)); if (par.isEmpty()) { dbs("ASSERT getLaneParentsChildren: parent not found"); return false; } p.append(par); root = p.first(); } // then find children c = git->getChildren(root); return true; } /** Iterator over all refnames of a given sha. * References are traversed in following order: * detached (name empty), local branches, remote branches, tags, other refs */ class RefNameIterator { Git* git; const QString sha; uint ref_types; // all reference types associated with sha int cur_state; // state indicating the currently processed ref type QStringList ref_names; // ref_names of current type QStringList::const_iterator cur_name; QString cur_branch; public: RefNameIterator(const QString &sha, Git* git); bool valid() const {return cur_state != -1;} QString name() const {return *cur_name;} int type() {return cur_state;} bool isCurrentBranch() {return *cur_name == cur_branch;} void next(); }; RefNameIterator::RefNameIterator(const QString &sha, Git *git) : git(git), sha(sha), cur_state(0), cur_branch(git->getCurrentBranchName()) { ref_types = git->checkRef(sha); if (ref_types == 0) { cur_state = -1; // indicates end return; } // initialize dummy string list ref_names << ""; cur_name = ref_names.begin(); // detached ? if ((ref_types & Git::CUR_BRANCH) && cur_branch.isEmpty()) { // indicate detached state with type() == 0 and empty ref name cur_branch = *cur_name; } else { // advance to first real ref name next(); } } void RefNameIterator::next() { ++cur_name; // switch to next ref type if required while (valid() && cur_name == ref_names.end()) { switch (cur_state) { case 0: cur_state = Git::BRANCH; break; case Git::BRANCH: cur_state = Git::RMT_BRANCH; break; case Git::RMT_BRANCH: cur_state = Git::TAG; break; case Git::TAG: cur_state = Git::REF; break; default: cur_state = -1; // indicate end } ref_names = git->getRefName(sha, (Git::RefType)cur_state); cur_name = ref_names.begin(); } } // ***************************************************************************** ListViewDelegate::ListViewDelegate(Git* g, ListViewProxy* px, QObject* p) : QItemDelegate(p) { git = g; lp = px; laneHeight = 0; diffTargetRow = -1; } QSize ListViewDelegate::sizeHint(const QStyleOptionViewItem&, const QModelIndex&) const { return QSize(laneWidth(), laneHeight); } void ListViewDelegate::diffTargetChanged(int row) { if (diffTargetRow != row) { diffTargetRow = row; emit updateView(); } } const Rev* ListViewDelegate::revLookup(int row, FileHistory** fhPtr) const { ListView* lv = static_cast(parent()); FileHistory* fh = static_cast(lv->model()); if (lp->sourceModel()) fh = static_cast(lp->sourceModel()); if (fhPtr) *fhPtr = fh; return git->revLookup(lv->sha(row), fh); } static QColor blend(const QColor& col1, const QColor& col2, int amount = 128) { // Returns ((256 - amount)*col1 + amount*col2) / 256; return QColor(((256 - amount)*col1.red() + amount*col2.red() ) / 256, ((256 - amount)*col1.green() + amount*col2.green()) / 256, ((256 - amount)*col1.blue() + amount*col2.blue() ) / 256); } void ListViewDelegate::paintGraphLane(QPainter* p, int type, int x1, int x2, const QColor& col, const QColor& activeCol, const QBrush& back) const { const int padding = 2; x1 += padding; x2 += padding; int h = laneHeight / 2; int m = (x1 + x2) / 2; int r = (x2 - x1) * 1 / 3; int d = 2 * r; #define P_CENTER m , h #define P_0 x2, h // > #define P_90 m , 0 // ^ #define P_180 x1, h // < #define P_270 m , 2 * h // v #define DELTA_UR 2*(x1 - m), 2*h , 0*16, 90*16 // -, #define DELTA_DR 2*(x1 - m), 2*-h, 270*16, 90*16 // -' #define DELTA_UL 2*(x2 - m), 2*h , 90*16, 90*16 // ,- #define DELTA_DL 2*(x2 - m), 2*-h, 180*16, 90*16 // '- #define CENTER_UR x1, 2*h, 225 #define CENTER_DR x1, 0 , 135 #define CENTER_UL x2, 2*h, 315 #define CENTER_DL x2, 0 , 45 #define R_CENTER m - r, h - r, d, d static QColor const & lanePenColor = QPalette().color(QPalette::WindowText); static QPen lanePen(lanePenColor, 2); // fast path here // arc switch (type) { case JOIN: case JOIN_R: case HEAD: case HEAD_R: { QConicalGradient gradient(CENTER_UR); gradient.setColorAt(0.375, col); gradient.setColorAt(0.625, activeCol); lanePen.setBrush(gradient); p->setPen(lanePen); p->drawArc(P_CENTER, DELTA_UR); break; } case JOIN_L: { QConicalGradient gradient(CENTER_UL); gradient.setColorAt(0.375, activeCol); gradient.setColorAt(0.625, col); lanePen.setBrush(gradient); p->setPen(lanePen); p->drawArc(P_CENTER, DELTA_UL); break; } case TAIL: case TAIL_R: { QConicalGradient gradient(CENTER_DR); gradient.setColorAt(0.375, activeCol); gradient.setColorAt(0.625, col); lanePen.setBrush(gradient); p->setPen(lanePen); p->drawArc(P_CENTER, DELTA_DR); break; } default: break; } lanePen.setColor(col); p->setPen(lanePen); // vertical line switch (type) { case ACTIVE: case NOT_ACTIVE: case MERGE_FORK: case MERGE_FORK_R: case MERGE_FORK_L: case JOIN: case JOIN_R: case JOIN_L: case CROSS: p->drawLine(P_90, P_270); break; case HEAD_L: case BRANCH: p->drawLine(P_CENTER, P_270); break; case TAIL_L: case INITIAL: case BOUNDARY: case BOUNDARY_C: case BOUNDARY_R: case BOUNDARY_L: p->drawLine(P_90, P_CENTER); break; default: break; } lanePen.setColor(activeCol); p->setPen(lanePen); // horizontal line switch (type) { case MERGE_FORK: case JOIN: case HEAD: case TAIL: case CROSS: case CROSS_EMPTY: case BOUNDARY_C: p->drawLine(P_180, P_0); break; case MERGE_FORK_R: case BOUNDARY_R: p->drawLine(P_180, P_CENTER); break; case MERGE_FORK_L: case HEAD_L: case TAIL_L: case BOUNDARY_L: p->drawLine(P_CENTER, P_0); break; default: break; } // center symbol, e.g. rect or ellipse switch (type) { case ACTIVE: case INITIAL: case BRANCH: p->setPen(Qt::black); p->setBrush(col); p->drawEllipse(R_CENTER); break; case MERGE_FORK: case MERGE_FORK_R: case MERGE_FORK_L: p->setPen(Qt::black); p->setBrush(col); p->drawRect(R_CENTER); break; case UNAPPLIED: // Red minus sign p->setPen(Qt::NoPen); p->setBrush(Qt::red); p->drawRect(m - r, h - 1, d, 2); break; case APPLIED: // Green plus sign p->setPen(Qt::NoPen); p->setBrush(DARK_GREEN); p->drawRect(m - r, h - 1, d, 2); p->drawRect(m - 1, h - r, 2, d); break; case BOUNDARY: p->setPen(Qt::black); p->setBrush(back); p->drawEllipse(R_CENTER); break; case BOUNDARY_C: case BOUNDARY_R: case BOUNDARY_L: p->setPen(Qt::black); p->setBrush(back); p->drawRect(R_CENTER); break; default: break; } #undef P_CENTER #undef P_0 #undef P_90 #undef P_180 #undef P_270 #undef DELTA_UR #undef DELTA_DR #undef DELTA_UL #undef DELTA_DL #undef CENTER_UR #undef CENTER_DR #undef CENTER_UL #undef CENTER_DL #undef R_CENTER } void ListViewDelegate::paintGraph(QPainter* p, const QStyleOptionViewItem& opt, const QModelIndex& i) const { static const QColor & baseColor = QPalette().color(QPalette::WindowText); static const QColor colors[COLORS_NUM] = { baseColor, Qt::red, DARK_GREEN, Qt::blue, Qt::darkGray, BROWN, Qt::magenta, ORANGE }; if (opt.state & QStyle::State_Selected) p->fillRect(opt.rect, opt.palette.highlight()); else if (i.row() & 1) p->fillRect(opt.rect, opt.palette.alternateBase()); else p->fillRect(opt.rect, opt.palette.base()); FileHistory* fh; const Rev* r = revLookup(i.row(), &fh); if (!r) return; p->save(); p->setClipRect(opt.rect, Qt::IntersectClip); p->translate(opt.rect.topLeft()); // calculate lanes if (r->lanes.count() == 0) git->setLane(r->sha(), fh); QBrush back = opt.palette.base(); const QVector& lanes(r->lanes); uint laneNum = lanes.count(); uint activeLane = 0; for (uint i = 0; i < laneNum; i++) if (isActive(lanes[i])) { activeLane = i; break; } int x1 = 0, x2 = 0; int maxWidth = opt.rect.width(); int lw = laneWidth(); QColor activeColor = colors[activeLane % COLORS_NUM]; if (opt.state & QStyle::State_Selected) activeColor = blend(activeColor, opt.palette.highlightedText().color(), 208); for (uint i = 0; i < laneNum && x2 < maxWidth; i++) { x1 = x2; x2 += lw; int ln = lanes[i]; if (ln == EMPTY) continue; QColor color = i == activeLane ? activeColor : colors[i % COLORS_NUM]; paintGraphLane(p, ln, x1, x2, color, activeColor, back); } p->restore(); } void ListViewDelegate::paintLog(QPainter* p, const QStyleOptionViewItem& opt, const QModelIndex& index) const { int row = index.row(); const Rev* r = revLookup(row); if (!r) return; if (r->isDiffCache) p->fillRect(opt.rect, changedFiles(ZERO_SHA) ? ORANGE : DARK_ORANGE); if (diffTargetRow == row) p->fillRect(opt.rect, LIGHT_BLUE); bool isHighlighted = lp->isHighlighted(row); QPixmap* pm = getTagMarks(r->sha(), opt); if (!pm && !isHighlighted) { // fast path in common case QItemDelegate::paint(p, opt, index); return; } QStyleOptionViewItem newOpt(opt); // we need a copy if (pm) { p->drawPixmap(newOpt.rect.x(), newOpt.rect.y() + 1, *pm); // +1 means leave a pixel spacing above the pixmap newOpt.rect.adjust(pm->width(), 0, 0, 0); delete pm; } if (isHighlighted) newOpt.font.setBold(true); QItemDelegate::paint(p, newOpt, index); } void ListViewDelegate::paint(QPainter* p, const QStyleOptionViewItem& opt, const QModelIndex& index) const { p->setRenderHints(QPainter::Antialiasing); if (index.column() == GRAPH_COL) return paintGraph(p, opt, index); if (index.column() == LOG_COL) return paintLog(p, opt, index); return QItemDelegate::paint(p, opt, index); } bool ListViewDelegate::changedFiles(SCRef sha) const { const RevFile* f = git->getFiles(sha); if (f) for (int i = 0; i < f->count(); i++) if (!f->statusCmp(i, RevFile::UNKNOWN)) return true; return false; } // adapt style and name based on type void getTagMarkParams(QString &name, QStyleOptionViewItem& o, const int type, const bool isCurrent) { QColor clr; switch (type) { case 0: name = "detached"; clr = Qt::red; break; case Git::BRANCH: clr = isCurrent ? Qt::green : DARK_GREEN; break; case Git::RMT_BRANCH: clr = LIGHT_ORANGE; break; case Git::TAG: clr = Qt::yellow; break; case Git::REF: clr = PURPLE; break; } o.palette.setColor(QPalette::Window, clr); o.palette.setColor(QPalette::WindowText, QColor(Qt::black)); o.font.setBold(isCurrent); } QPixmap* ListViewDelegate::getTagMarks(SCRef sha, const QStyleOptionViewItem& opt) const { uint rt = git->checkRef(sha); if (rt == 0) return NULL; // common case: no refs at all QPixmap* pm = new QPixmap(); // must be deleted by caller for (RefNameIterator it(sha, git); it.valid(); it.next()) { QStyleOptionViewItem o(opt); QString name = it.name(); getTagMarkParams(name, o, it.type(), it.isCurrentBranch()); addTextPixmap(&pm, name, o); } return pm; } QString ListView::refNameAt(const QPoint &pos) { QModelIndex index = indexAt(pos); if (index.column() != LOG_COL) return QString(); int spacing = 4; // inner spacing within pixmaps (cf. addTextPixmap) int ofs = visualRect(index).left(); for (RefNameIterator it(sha(index.row()), git); it.valid(); it.next()) { QStyleOptionViewItem o; QString name = it.name(); getTagMarkParams(name, o, it.type(), it.isCurrentBranch()); QFontMetrics fm(o.font); ofs += fm.boundingRect(name).width() + 2*spacing; if (pos.x() <= ofs) { // name found: return fully-qualified ref name (cf. Git::getRefs() for names) switch (it.type()) { case Git::BRANCH: return it.name(); break; case Git::TAG: return "tags/" + it.name(); break; case Git::RMT_BRANCH: return "remotes/" + it.name(); break; case Git::REF: return "bases/" + it.name(); break; default: return QString(); break; } } ofs += 2; // distance between pixmaps (cf. addTextPixmap) } return QString(); } void ListViewDelegate::addTextPixmap(QPixmap** pp, SCRef txt, const QStyleOptionViewItem& opt) const { QPixmap* pm = *pp; int ofs = pm->isNull() ? 0 : pm->width() + 2; int spacing = 4; QFontMetrics fm(opt.font); int pw = fm.boundingRect(txt).width() + 2 * spacing; int ph = fm.height(); QPixmap* newPm = new QPixmap(ofs + pw, ph); QPainter p; p.begin(newPm); if (!pm->isNull()) { newPm->fill(opt.palette.base().color()); p.drawPixmap(0, 0, *pm); } p.setPen(opt.palette.color(QPalette::WindowText)); p.setBrush(opt.palette.color(QPalette::Window)); p.setFont(opt.font); p.drawRect(ofs, 0, pw - 1, ph - 1); p.drawText(ofs + spacing, fm.ascent(), txt); p.end(); delete pm; *pp = newPm; } // ***************************************************************************** ListViewProxy::ListViewProxy(QObject* p, Domain * dm, Git * g) : QSortFilterProxyModel(p) { d = dm; git = g; colNum = 0; isHighLight = false; setDynamicSortFilter(false); } bool ListViewProxy::isMatch(SCRef sha) const { if (colNum == SHA_MAP_COL) // in this case shaMap contains all good sha to search for return shaSet.contains(sha); const Rev* r = git->revLookup(sha); if (!r) { dbp("ASSERT in ListViewFilter::isMatch, sha <%1> not found", sha); return false; } QString target; if (colNum == LOG_COL) target = r->shortLog(); else if (colNum == AUTH_COL) target = r->author(); else if (colNum == LOG_MSG_COL) target = r->longLog(); else if (colNum == COMMIT_COL) target = sha; // wildcard search, case insensitive return (target.contains(filter)); } bool ListViewProxy::isMatch(int source_row) const { FileHistory* fh = d->model(); if (fh->rowCount() <= source_row) // FIXME required to avoid an ASSERT in d->isMatch() return false; bool extFilter = (colNum == -1); return ((!extFilter && isMatch(fh->sha(source_row))) ||( extFilter && d->isMatch(fh->sha(source_row)))); } bool ListViewProxy::isHighlighted(int row) const { // FIXME row == source_row only because when // higlights the rows are not hidden return (isHighLight && isMatch(row)); } bool ListViewProxy::filterAcceptsRow(int source_row, const QModelIndex&) const { return (isHighLight || isMatch(source_row)); } int ListViewProxy::setFilter(bool isOn, bool h, SCRef fl, int cn, ShaSet* s) { filter = QRegExp(fl, Qt::CaseInsensitive, QRegExp::Wildcard); colNum = cn; if (s) shaSet = *s; // isHighlighted() is called also when filter is off, // so reset 'isHighLight' flag in that case isHighLight = h && isOn; ListView* lv = static_cast(parent()); FileHistory* fh = d->model(); if (!isOn && sourceModel()){ lv->setModel(fh); setSourceModel(NULL); } else if (isOn && !isHighLight) { setSourceModel(fh); // trigger a rows scanning lv->setModel(this); } return (sourceModel() ? rowCount() : 0); } qgit-2.7/src/listview.h000066400000000000000000000075351305655150700151550ustar00rootroot00000000000000/* Author: Marco Costalba (C) 2005-2007 Copyright: See COPYING file that comes with this distribution */ #ifndef LISTVIEW_H #define LISTVIEW_H #include #include #include #include #include "common.h" class Git; class StateInfo; class Domain; class FileHistory; class ListViewProxy; class ListView: public QTreeView { Q_OBJECT public: ListView(QWidget* parent); ~ListView(); void setup(Domain* d, Git* g); const QString shaFromAnnId(uint id); void showIdValues(); void scrollToCurrent(ScrollHint hint = EnsureVisible); void scrollToNextHighlighted(int direction); void getSelectedItems(QStringList& selectedItems); bool update(); void addNewRevs(const QVector& shaVec); const QString currentText(int col); int filterRows(bool, bool, SCRef = QString(), int = -1, ShaSet* = NULL); const QString sha(int row) const; int row(SCRef sha) const; QString refNameAt(const QPoint &pos); const QString& selectedRefName() const {return lastRefName;} void markDiffToSha(SCRef sha); signals: void lanesContextMenuRequested(const QStringList&, const QStringList&); void revisionsDragged(const QStringList&); void revisionsDropped(const QStringList&); void contextMenu(const QString&, int); void diffTargetChanged(int); // used by new model_view integration void showStatusMessage(const QString&); public slots: void on_changeFont(const QFont& f); void on_keyUp(); void on_keyDown(); protected: virtual void mousePressEvent(QMouseEvent* e); virtual void mouseMoveEvent(QMouseEvent* e); virtual void mouseReleaseEvent(QMouseEvent* e); virtual void dragEnterEvent(QDragEnterEvent* e); virtual void dragMoveEvent(QDragMoveEvent* e); virtual void dropEvent(QDropEvent* e); private slots: void on_customContextMenuRequested(const QPoint&); virtual void currentChanged(const QModelIndex&, const QModelIndex&); private: void setupGeometry(); bool filterRightButtonPressed(QMouseEvent* e); bool getLaneParentsChildren(SCRef sha, int x, SList p, SList c); int getLaneType(SCRef sha, int pos) const; Domain* d; Git* git; StateInfo* st; FileHistory* fh; ListViewProxy* lp; unsigned long secs; bool filterNextContextMenuRequest; QString lastRefName; // last ref name clicked on }; class ListViewDelegate : public QItemDelegate { Q_OBJECT public: ListViewDelegate(Git* git, ListViewProxy* lp, QObject* parent); virtual void paint(QPainter* p, const QStyleOptionViewItem& o, const QModelIndex &i) const; virtual QSize sizeHint(const QStyleOptionViewItem& o, const QModelIndex &i) const; int laneWidth() const { return 3 * laneHeight / 4; } void setLaneHeight(int h) { laneHeight = h; } signals: void updateView(); public slots: void diffTargetChanged(int); private: const Rev* revLookup(int row, FileHistory** fhPtr = NULL) const; void paintLog(QPainter* p, const QStyleOptionViewItem& o, const QModelIndex &i) const; void paintGraph(QPainter* p, const QStyleOptionViewItem& o, const QModelIndex &i) const; void paintGraphLane(QPainter* p, int type, int x1, int x2, const QColor& col, const QColor& activeCol, const QBrush& back) const; QPixmap* getTagMarks(SCRef sha, const QStyleOptionViewItem& opt) const; void addTextPixmap(QPixmap** pp, SCRef txt, const QStyleOptionViewItem& opt) const; bool changedFiles(SCRef sha) const; Git* git; ListViewProxy* lp; int laneHeight; int diffTargetRow; }; class ListViewProxy : public QSortFilterProxyModel { Q_OBJECT public: ListViewProxy(QObject* parent, Domain* d, Git* g); int setFilter(bool isOn, bool highlight, SCRef filter, int colNum, ShaSet* s); bool isHighlighted(int row) const; protected: virtual bool filterAcceptsRow(int sourceRow, const QModelIndex& sourceParent) const; private: bool isMatch(int row) const; bool isMatch(SCRef sha) const; Domain* d; Git* git; bool isHighLight; QRegExp filter; int colNum; ShaSet shaSet; }; #endif qgit-2.7/src/mainimpl.cpp000066400000000000000000001727541305655150700154560ustar00rootroot00000000000000/* Description: qgit main view Author: Marco Costalba (C) 2005-2007 Copyright: See COPYING file that comes with this distribution */ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "config.h" // defines PACKAGE_VERSION #include "consoleimpl.h" #include "commitimpl.h" #include "common.h" #include "customactionimpl.h" #include "fileview.h" #include "git.h" #include "help.h" #include "listview.h" #include "mainimpl.h" #include "inputdialog.h" #include "patchview.h" #include "rangeselectimpl.h" #include "revdesc.h" #include "revsview.h" #include "settingsimpl.h" #include "treeview.h" #include "ui_help.h" #include "ui_revsview.h" #include "ui_fileview.h" #include "ui_patchview.h" using namespace QGit; MainImpl::MainImpl(SCRef cd, QWidget* p) : QMainWindow(p) { EM_INIT(exExiting, "Exiting"); setAttribute(Qt::WA_DeleteOnClose); setupUi(this); // manual setup widgets not buildable with Qt designer lineEditSHA = new QLineEdit(NULL); lineEditFilter = new QLineEdit(NULL); cmbSearch = new QComboBox(NULL); QString list("Short log,Log msg,Author,SHA1,File,Patch,Patch (regExp)"); cmbSearch->addItems(list.split(",")); toolBar->addWidget(lineEditSHA); QAction* act = toolBar->insertWidget(ActSearchAndFilter, lineEditFilter); toolBar->insertWidget(act, cmbSearch); connect(lineEditSHA, SIGNAL(returnPressed()), this, SLOT(lineEditSHA_returnPressed())); connect(lineEditFilter, SIGNAL(returnPressed()), this, SLOT(lineEditFilter_returnPressed())); // create light and dark colors for alternate background ODD_LINE_COL = palette().color(QPalette::Base); EVEN_LINE_COL = ODD_LINE_COL.dark(103); // our interface to git world git = new Git(this); setupShortcuts(); qApp->installEventFilter(this); // init native types setRepositoryBusy = false; // init filter match highlighters shortLogRE.setMinimal(true); shortLogRE.setCaseSensitivity(Qt::CaseInsensitive); longLogRE.setMinimal(true); longLogRE.setCaseSensitivity(Qt::CaseInsensitive); // set-up standard revisions and files list font QSettings settings; QString font(settings.value(STD_FNT_KEY).toString()); if (font.isEmpty()) font = QApplication::font().toString(); QGit::STD_FONT.fromString(font); // set-up typewriter (fixed width) font font = settings.value(TYPWRT_FNT_KEY).toString(); if (font.isEmpty()) { // choose a sensible default QFont fnt = QApplication::font(); fnt.setStyleHint(QFont::TypeWriter, QFont::PreferDefault); fnt.setFixedPitch(true); fnt.setFamily(fnt.defaultFamily()); // the family corresponding font = fnt.toString(); // to current style hint } QGit::TYPE_WRITER_FONT.fromString(font); // set-up tab view delete tabWdg->currentWidget(); // cannot be done in Qt Designer rv = new RevsView(this, git, true); // set has main domain tabWdg->addTab(rv->tabPage(), "&Rev list"); // set-up tab corner widget ('close tab' button) QToolButton* ct = new QToolButton(tabWdg); ct->setIcon(QIcon(QString::fromUtf8(":/icons/resources/tab_remove.png"))); ct->setToolTip("Close tab"); ct->setEnabled(false); tabWdg->setCornerWidget(ct); connect(ct, SIGNAL(clicked()), this, SLOT(pushButtonCloseTab_clicked())); connect(this, SIGNAL(closeTabButtonEnabled(bool)), ct, SLOT(setEnabled(bool))); // set-up file names loading progress bar pbFileNamesLoading = new QProgressBar(statusBar()); pbFileNamesLoading->setTextVisible(false); pbFileNamesLoading->setToolTip("Background file names loading"); pbFileNamesLoading->hide(); statusBar()->addPermanentWidget(pbFileNamesLoading); QVector v(1, treeSplitter); QGit::restoreGeometrySetting(QGit::MAIN_GEOM_KEY, this, &v); treeView->hide(); // set-up menu for recent visited repositories connect(File, SIGNAL(triggered(QAction*)), this, SLOT(openRecent_triggered(QAction*))); doUpdateRecentRepoMenu(""); // set-up menu for custom actions connect(Actions, SIGNAL(triggered(QAction*)), this, SLOT(customAction_triggered(QAction*))); doUpdateCustomActionMenu(settings.value(ACT_LIST_KEY).toStringList()); // manual adjust lineEditSHA width QString tmp(41, '8'); int wd = lineEditSHA->fontMetrics().boundingRect(tmp).width(); lineEditSHA->setMinimumWidth(wd); // disable all actions updateGlobalActions(false); connect(git, SIGNAL(fileNamesLoad(int, int)), this, SLOT(fileNamesLoad(int, int))); connect(git, SIGNAL(newRevsAdded(const FileHistory*, const QVector&)), this, SLOT(newRevsAdded(const FileHistory*, const QVector&))); connect(this, SIGNAL(typeWriterFontChanged()), this, SIGNAL(updateRevDesc())); connect(this, SIGNAL(changeFont(const QFont&)), git, SIGNAL(changeFont(const QFont&))); // connect cross-domain update signals connect(rv->tab()->listViewLog, SIGNAL(doubleClicked(const QModelIndex&)), this, SLOT(listViewLog_doubleClicked(const QModelIndex&))); connect(rv->tab()->listViewLog, SIGNAL(showStatusMessage(const QString&)), statusBar(), SLOT(showMessage(const QString&))); connect(rv->tab()->fileList, SIGNAL(itemDoubleClicked(QListWidgetItem*)), this, SLOT(fileList_itemDoubleClicked(QListWidgetItem*))); connect(treeView, SIGNAL(itemDoubleClicked(QTreeWidgetItem*, int)), this, SLOT(treeView_doubleClicked(QTreeWidgetItem*, int))); // use most recent repo as startup dir if it exists and user opted to do so QStringList recents(settings.value(REC_REP_KEY).toStringList()); QDir checkRepo; if ( recents.size() >= 1 && testFlag(REOPEN_REPO_F, FLAGS_KEY) && checkRepo.exists(recents.at(0))) { startUpDir = recents.at(0); } else { startUpDir = (cd.isEmpty() ? QDir::current().absolutePath() : cd); } // MainImpl c'tor is called before to enter event loop, // but some stuff requires event loop to init properly QTimer::singleShot(10, this, SLOT(initWithEventLoopActive())); } void MainImpl::initWithEventLoopActive() { git->checkEnvironment(); setRepository(startUpDir); startUpDir = ""; // one shot } void MainImpl::saveCurrentGeometry() { QVector v(1, treeSplitter); QGit::saveGeometrySetting(QGit::MAIN_GEOM_KEY, this, &v); } void MainImpl::highlightAbbrevSha(SCRef abbrevSha) { // reset any previous highlight if (ActSearchAndHighlight->isChecked()) ActSearchAndHighlight->toggle(); // set to highlight on SHA matching cmbSearch->setCurrentIndex(CS_SHA1); // set substring to search for lineEditFilter->setText(abbrevSha); // go with highlighting ActSearchAndHighlight->toggle(); } void MainImpl::lineEditSHA_returnPressed() { QString sha = git->getRefSha(lineEditSHA->text()); if (!sha.isEmpty()) // good, we can resolve to an unique sha { rv->st.setSha(sha); UPDATE_DOMAIN(rv); } else { // try a multiple match search highlightAbbrevSha(lineEditSHA->text()); goMatch(0); } } void MainImpl::ActBack_activated() { lineEditSHA->undo(); // first for insert(text) if (lineEditSHA->text().isEmpty()) lineEditSHA->undo(); // double undo, see RevsView::updateLineEditSHA() lineEditSHA_returnPressed(); } void MainImpl::ActForward_activated() { lineEditSHA->redo(); if (lineEditSHA->text().isEmpty()) lineEditSHA->redo(); lineEditSHA_returnPressed(); } // *************************** ExternalDiffViewer *************************** void MainImpl::ActExternalDiff_activated() { QStringList args; QStringList filenames; getExternalDiffArgs(&args, &filenames); ExternalDiffProc* externalDiff = new ExternalDiffProc(filenames, this); externalDiff->setWorkingDirectory(curDir); if (!QGit::startProcess(externalDiff, args)) { QString text("Cannot start external viewer: "); text.append(args[0]); QMessageBox::warning(this, "Error - QGit", text); delete externalDiff; } } void MainImpl::getExternalDiffArgs(QStringList* args, QStringList* filenames) { // save files to diff in working directory, // will be removed by ExternalDiffProc on exit QFileInfo f(rv->st.fileName()); QString prevRevSha(rv->st.diffToSha()); if (prevRevSha.isEmpty()) { // default to first parent const Rev* r = git->revLookup(rv->st.sha()); prevRevSha = (r && r->parentsCount() > 0 ? r->parent(0) : rv->st.sha()); } QFileInfo fi(f); QString fName1(curDir + "/" + rv->st.sha().left(6) + "_" + fi.fileName()); QString fName2(curDir + "/" + prevRevSha.left(6) + "_" + fi.fileName()); QApplication::setOverrideCursor(QCursor(Qt::WaitCursor)); QByteArray fileContent; QTextCodec* tc = QTextCodec::codecForLocale(); QString fileSha(git->getFileSha(rv->st.fileName(), rv->st.sha())); git->getFile(fileSha, NULL, &fileContent, rv->st.fileName()); if (!writeToFile(fName1, tc->toUnicode(fileContent))) statusBar()->showMessage("Unable to save " + fName1); fileSha = git->getFileSha(rv->st.fileName(), prevRevSha); git->getFile(fileSha, NULL, &fileContent, rv->st.fileName()); if (!writeToFile(fName2, tc->toUnicode(fileContent))) statusBar()->showMessage("Unable to save " + fName2); // get external diff viewer command QSettings settings; QString extDiff(settings.value(EXT_DIFF_KEY, EXT_DIFF_DEF).toString()); QApplication::restoreOverrideCursor(); // if command doesn't have %1 and %2 to denote filenames, add them to end if (!extDiff.contains("%1")) { extDiff.append(" %1"); } if (!extDiff.contains("%2")) { extDiff.append(" %2"); } // set process arguments QStringList extDiffArgs = extDiff.split(' '); QString curArg; for (int i = 0; i < extDiffArgs.count(); i++) { curArg = extDiffArgs.value(i); // perform any filename replacements that are necessary // (done inside the loop to handle whitespace in paths properly) curArg.replace("%1", fName2); curArg.replace("%2", fName1); args->append(curArg); } // set filenames so that they can be deleted when the process completes filenames->append(fName1); filenames->append(fName2); } // *************************** ExternalEditor *************************** void MainImpl::ActExternalEditor_activated() { const QStringList &args = getExternalEditorArgs(); ExternalEditorProc* externalEditor = new ExternalEditorProc(this); externalEditor->setWorkingDirectory(curDir); if (!QGit::startProcess(externalEditor, args)) { QString text("Cannot start external editor: "); text.append(args[0]); QMessageBox::warning(this, "Error - QGit", text); delete externalEditor; } } QStringList MainImpl::getExternalEditorArgs() { QString fName1(curDir + "/" + rv->st.fileName()); // get external diff viewer command QSettings settings; QString extEditor(settings.value(EXT_EDITOR_KEY, EXT_EDITOR_DEF).toString()); // if command doesn't have %1 to denote filename, add to end if (!extEditor.contains("%1")) extEditor.append(" %1"); // set process arguments QStringList args = extEditor.split(' '); for (int i = 0; i < args.count(); i++) { QString &curArg = args[i]; // perform any filename replacements that are necessary // (done inside the loop to handle whitespace in paths properly) curArg.replace("%1", fName1); } return args; } // ********************** Repository open or changed ************************* void MainImpl::setRepository(SCRef newDir, bool refresh, bool keepSelection, const QStringList* passedArgs, bool overwriteArgs) { /* Because Git::init calls processEvents(), if setRepository() is called in a tight loop (as example keeping pressed F5 refresh button) then a lot of pending init() calls would be stacked. On returning from processEvents() an exception is trown and init is exited, so we end up with a long list of 'exception thrown' messages. But the worst thing is that we have to wait for _all_ the init call to exit and this could take a long time as example in case of working directory refreshing 'git update-index' of a big tree. So we use a guard flag to guarantee we have only one init() call 'in flight' */ if (setRepositoryBusy) return; setRepositoryBusy = true; // check for a refresh or open of a new repository while in filtered view if (ActFilterTree->isChecked() && passedArgs == NULL) // toggle() triggers a refresh and a following setRepository() // call that is filtered out by setRepositoryBusy guard flag ActFilterTree->toggle(); // triggers ActFilterTree_toggled() try { EM_REGISTER(exExiting); bool archiveChanged; git->getBaseDir(newDir, curDir, archiveChanged); git->stop(archiveChanged); // stop all pending processes, non blocking if (archiveChanged && refresh) dbs("ASSERT in setRepository: different dir with no range select"); // now we can clear all our data setWindowTitle(curDir + " - QGit"); bool complete = !refresh || !keepSelection; rv->clear(complete); if (archiveChanged) emit closeAllTabs(); // disable all actions updateGlobalActions(false); updateContextActions("", "", false, false); ActCommit_setEnabled(false); if (ActFilterTree->isChecked()) setWindowTitle(windowTitle() + " - FILTER ON < " + passedArgs->join(" ") + " >"); // tree name should be set before init because in case of // StGIT archives the first revs are sent before init returns QString n(curDir); treeView->setTreeName(n.prepend('/').section('/', -1, -1)); bool quit; bool ok = git->init(curDir, !refresh, passedArgs, overwriteArgs, &quit); // blocking call if (quit) goto exit; updateCommitMenu(ok && git->isStGITStack()); ActCheckWorkDir->setChecked(testFlag(DIFF_INDEX_F)); // could be changed in Git::init() if (ok) { updateGlobalActions(true); if (archiveChanged) updateRecentRepoMenu(curDir); } else statusBar()->showMessage("Not a git archive"); exit: setRepositoryBusy = false; EM_REMOVE(exExiting); if (quit && !startUpDir.isEmpty()) close(); } catch (int i) { EM_REMOVE(exExiting); if (EM_MATCH(i, exExiting, "loading repository")) { EM_THROW_PENDING; return; } const QString info("Exception \'" + EM_DESC(i) + "\' not " "handled in setRepository...re-throw"); dbs(info); throw; } } void MainImpl::updateGlobalActions(bool b) { ActRefresh->setEnabled(b); ActCheckWorkDir->setEnabled(b); ActViewRev->setEnabled(b); ActViewDiff->setEnabled(b); ActViewDiffNewTab->setEnabled(b && firstTab()); ActShowTree->setEnabled(b); ActMailApplyPatch->setEnabled(b); ActMailFormatPatch->setEnabled(b); rv->setEnabled(b); } const QString REV_LOCAL_BRANCHES("REV_LOCAL_BRANCHES"); const QString REV_REMOTE_BRANCHES("REV_REMOTE_BRANCHES"); const QString REV_TAGS("REV_TAGS"); const QString CURRENT_BRANCH("CURRENT_BRANCH"); const QString SELECTED_NAME("SELECTED_NAME"); void MainImpl::updateRevVariables(SCRef sha) { QMap &v = revision_variables; v.clear(); const QStringList &remote_branches = git->getRefName(sha, Git::RMT_BRANCH); QString curBranch; v.insert(REV_LOCAL_BRANCHES, git->getRefName(sha, Git::BRANCH)); v.insert(CURRENT_BRANCH, git->getCurrentBranchName()); v.insert(REV_REMOTE_BRANCHES, remote_branches); v.insert(REV_TAGS, git->getRefName(sha, Git::TAG)); v.insert("SHA", sha); // determine which name the user clicked on ListView* lv = rv->tab()->listViewLog; v.insert(SELECTED_NAME, lv->selectedRefName()); } void MainImpl::updateContextActions(SCRef newRevSha, SCRef newFileName, bool isDir, bool found) { bool pathActionsEnabled = !newFileName.isEmpty(); bool fileActionsEnabled = (pathActionsEnabled && !isDir); ActViewFile->setEnabled(fileActionsEnabled); ActViewFileNewTab->setEnabled(fileActionsEnabled && firstTab()); ActExternalDiff->setEnabled(fileActionsEnabled); ActExternalEditor->setEnabled(fileActionsEnabled); ActSaveFile->setEnabled(fileActionsEnabled); ActFilterTree->setEnabled(pathActionsEnabled || ActFilterTree->isChecked()); // bool isTag = false; bool isUnApplied = false; bool isApplied = false; uint ref_type = 0; if (found) { const Rev* r = git->revLookup(newRevSha); ref_type = git->checkRef(newRevSha, Git::ANY_REF); // isTag = ref_type & Git::TAG; isUnApplied = r->isUnApplied; isApplied = r->isApplied; } ActMarkDiffToSha->setEnabled(newRevSha != ZERO_SHA); ActCheckout->setEnabled(found && (newRevSha != ZERO_SHA) && !isUnApplied); ActBranch->setEnabled(found && (newRevSha != ZERO_SHA) && !isUnApplied); ActTag->setEnabled(found && (newRevSha != ZERO_SHA) && !isUnApplied); ActDelete->setEnabled(ref_type != 0); ActPush->setEnabled(found && isUnApplied && git->isNothingToCommit()); ActPop->setEnabled(found && isApplied && git->isNothingToCommit()); } // ************************* cross-domain update Actions *************************** void MainImpl::listViewLog_doubleClicked(const QModelIndex& index) { if (index.isValid() && ActViewDiff->isEnabled()) ActViewDiff->activate(QAction::Trigger); } void MainImpl::histListView_doubleClicked(const QModelIndex& index) { if (index.isValid() && ActViewRev->isEnabled()) ActViewRev->activate(QAction::Trigger); } void MainImpl::fileList_itemDoubleClicked(QListWidgetItem* item) { bool isFirst = (item && item->listWidget()->item(0) == item); if (isFirst && rv->st.isMerge()) return; if (testFlag(OPEN_IN_EDITOR_F, FLAGS_KEY)) { if (item && ActExternalEditor->isEnabled()) ActExternalEditor->activate(QAction::Trigger); } else { bool isMainView = (item && item->listWidget() == rv->tab()->fileList); if (isMainView && ActViewDiff->isEnabled()) ActViewDiff->activate(QAction::Trigger); if (item && !isMainView && ActViewFile->isEnabled()) ActViewFile->activate(QAction::Trigger); } } void MainImpl::treeView_doubleClicked(QTreeWidgetItem* item, int) { if (testFlag(OPEN_IN_EDITOR_F, FLAGS_KEY)) { if (item && ActExternalEditor->isEnabled()) ActExternalEditor->activate(QAction::Trigger); } else { if (item && ActViewFile->isEnabled()) ActViewFile->activate(QAction::Trigger); } } void MainImpl::pushButtonCloseTab_clicked() { Domain* t; switch (currentTabType(&t)) { case TAB_REV: break; case TAB_PATCH: t->deleteWhenDone(); ActViewDiffNewTab->setEnabled(ActViewDiff->isEnabled() && firstTab()); break; case TAB_FILE: t->deleteWhenDone(); ActViewFileNewTab->setEnabled(ActViewFile->isEnabled() && firstTab()); break; default: dbs("ASSERT in pushButtonCloseTab_clicked: unknown current page"); break; } } void MainImpl::ActRangeDlg_activated() { QString args; RangeSelectImpl rs(this, &args, false, git); bool quit = (rs.exec() == QDialog::Rejected); // modal execution if (!quit) { const QStringList l(args.split(" ")); setRepository(curDir, true, true, &l, true); } } void MainImpl::ActViewRev_activated() { Domain* t; if (currentTabType(&t) == TAB_FILE) { rv->st = t->st; UPDATE_DOMAIN(rv); } tabWdg->setCurrentWidget(rv->tabPage()); } void MainImpl::ActViewFile_activated() { openFileTab(firstTab()); } void MainImpl::ActViewFileNewTab_activated() { openFileTab(); } void MainImpl::openFileTab(FileView* fv) { if (!fv) { fv = new FileView(this, git); tabWdg->addTab(fv->tabPage(), "File"); connect(fv->tab()->histListView, SIGNAL(doubleClicked(const QModelIndex&)), this, SLOT(histListView_doubleClicked(const QModelIndex&))); connect(this, SIGNAL(closeAllTabs()), fv, SLOT(on_closeAllTabs())); ActViewFileNewTab->setEnabled(ActViewFile->isEnabled()); } tabWdg->setCurrentWidget(fv->tabPage()); fv->st = rv->st; UPDATE_DOMAIN(fv); } void MainImpl::ActViewDiff_activated() { Domain* t; if (currentTabType(&t) == TAB_FILE) { rv->st = t->st; UPDATE_DOMAIN(rv); } rv->viewPatch(false); ActViewDiffNewTab->setEnabled(true); if (ActSearchAndFilter->isChecked() || ActSearchAndHighlight->isChecked()) { bool isRegExp = (cmbSearch->currentIndex() == CS_PATCH_REGEXP); emit highlightPatch(lineEditFilter->text(), isRegExp); } } void MainImpl::ActViewDiffNewTab_activated() { rv->viewPatch(true); } bool MainImpl::eventFilter(QObject* obj, QEvent* ev) { if (ev->type() == QEvent::Wheel) { QWheelEvent* e = static_cast(ev); if (e->modifiers() == Qt::AltModifier) { int idx = tabWdg->currentIndex(); if (e->delta() < 0) idx = (++idx == tabWdg->count() ? 0 : idx); else idx = (--idx < 0 ? tabWdg->count() - 1 : idx); tabWdg->setCurrentIndex(idx); return true; } } return QWidget::eventFilter(obj, ev); } void MainImpl::revisionsDragged(SCList selRevs) { const QString h(QString::fromLatin1("@") + curDir + '\n'); const QString dragRevs = selRevs.join(h).append(h).trimmed(); QDrag* drag = new QDrag(this); QMimeData* mimeData = new QMimeData; mimeData->setText(dragRevs); drag->setMimeData(mimeData); drag->start(); // blocking until drop event } void MainImpl::revisionsDropped(SCList remoteRevs) { // remoteRevs is already sanity checked to contain some possible valid data if (rv->isDropping()) // avoid reentrancy return; QDir dr(curDir + QGit::PATCHES_DIR); if (dr.exists()) { const QString tmp("Please remove stale import directory " + dr.absolutePath()); statusBar()->showMessage(tmp); return; } bool workDirOnly, fold; if (!askApplyPatchParameters(&workDirOnly, &fold)) return; // ok, let's go rv->setDropping(true); dr.setFilter(QDir::Files); QApplication::setOverrideCursor(QCursor(Qt::WaitCursor)); raise(); EM_PROCESS_EVENTS; uint revNum = 0; QStringList::const_iterator it(remoteRevs.constEnd()); do { --it; QString tmp("Importing revision %1 of %2"); statusBar()->showMessage(tmp.arg(++revNum).arg(remoteRevs.count())); SCRef sha((*it).section('@', 0, 0)); SCRef remoteRepo((*it).section('@', 1)); if (!dr.exists(remoteRepo)) break; // we create patches one by one if (!git->formatPatch(QStringList(sha), dr.absolutePath(), remoteRepo)) break; dr.refresh(); if (dr.count() != 1) { qDebug("ASSERT in on_droppedRevisions: found %i files " "in %s", dr.count(), QGit::PATCHES_DIR.toLatin1().constData()); break; } SCRef fn(dr.absoluteFilePath(dr[0])); bool is_applied = git->applyPatchFile(fn, fold, Git::optDragDrop); dr.remove(fn); if (!is_applied) break; } while (it != remoteRevs.constBegin()); if (it == remoteRevs.constBegin()) statusBar()->clearMessage(); else statusBar()->showMessage("Failed to import revision " + QString::number(revNum--)); if (workDirOnly && (revNum > 0)) git->resetCommits(revNum); dr.rmdir(dr.absolutePath()); // 'dr' must be already empty QApplication::restoreOverrideCursor(); rv->setDropping(false); refreshRepo(); } // ******************************* Filter ****************************** void MainImpl::newRevsAdded(const FileHistory* fh, const QVector&) { if (!git->isMainHistory(fh)) return; if (ActSearchAndFilter->isChecked()) ActSearchAndFilter_toggled(true); // filter again on new arrived data if (ActSearchAndHighlight->isChecked()) ActSearchAndHighlight_toggled(true); // filter again on new arrived data // first rev could be a StGIT unapplied patch so check more then once if ( !ActCommit->isEnabled() && (!git->isNothingToCommit() || git->isUnknownFiles())) ActCommit_setEnabled(true); } void MainImpl::lineEditFilter_returnPressed() { ActSearchAndFilter->setChecked(true); } void MainImpl::ActSearchAndFilter_toggled(bool isOn) { ActSearchAndHighlight->setEnabled(!isOn); ActSearchAndFilter->setEnabled(false); filterList(isOn, false); // blocking call ActSearchAndFilter->setEnabled(true); } void MainImpl::ActSearchAndHighlight_toggled(bool isOn) { ActSearchAndFilter->setEnabled(!isOn); ActSearchAndHighlight->setEnabled(false); filterList(isOn, true); // blocking call ActSearchAndHighlight->setEnabled(true); } void MainImpl::filterList(bool isOn, bool onlyHighlight) { lineEditFilter->setEnabled(!isOn); cmbSearch->setEnabled(!isOn); SCRef filter(lineEditFilter->text()); if (filter.isEmpty()) return; ShaSet shaSet; bool patchNeedsUpdate, isRegExp; patchNeedsUpdate = isRegExp = false; int idx = cmbSearch->currentIndex(), colNum = 0; if (isOn) { switch (idx) { case CS_SHORT_LOG: colNum = LOG_COL; shortLogRE.setPattern(filter); break; case CS_LOG_MSG: colNum = LOG_MSG_COL; longLogRE.setPattern(filter); break; case CS_AUTHOR: colNum = AUTH_COL; break; case CS_SHA1: colNum = COMMIT_COL; break; case CS_FILE: case CS_PATCH: case CS_PATCH_REGEXP: colNum = SHA_MAP_COL; QApplication::setOverrideCursor(QCursor(Qt::WaitCursor)); EM_PROCESS_EVENTS; // to paint wait cursor if (idx == CS_FILE) git->getFileFilter(filter, shaSet); else { isRegExp = (idx == CS_PATCH_REGEXP); if (!git->getPatchFilter(filter, isRegExp, shaSet)) { QApplication::restoreOverrideCursor(); ActSearchAndFilter->toggle(); return; } patchNeedsUpdate = (shaSet.count() > 0); } QApplication::restoreOverrideCursor(); break; } } else { patchNeedsUpdate = (idx == CS_PATCH || idx == CS_PATCH_REGEXP); shortLogRE.setPattern(""); longLogRE.setPattern(""); } QApplication::setOverrideCursor(QCursor(Qt::WaitCursor)); ListView* lv = rv->tab()->listViewLog; int matchedCnt = lv->filterRows(isOn, onlyHighlight, filter, colNum, &shaSet); QApplication::restoreOverrideCursor(); emit updateRevDesc(); // could be highlighted if (patchNeedsUpdate) emit highlightPatch(isOn ? filter : "", isRegExp); QString msg; if (isOn && !onlyHighlight) msg = QString("Found %1 matches. Toggle filter/highlight " "button to remove the filter").arg(matchedCnt); QApplication::postEvent(rv, new MessageEvent(msg)); // deferred message, after update } bool MainImpl::event(QEvent* e) { BaseEvent* de = dynamic_cast(e); if (!de) return QWidget::event(e); SCRef data = de->myData(); bool ret = true; switch ((EventType)e->type()) { case ERROR_EV: { QApplication::setOverrideCursor(QCursor(Qt::ArrowCursor)); EM_PROCESS_EVENTS; MainExecErrorEvent* me = (MainExecErrorEvent*)e; QString text("An error occurred while executing command:\n\n"); text.append(me->command() + "\n\nGit says: \n\n" + me->report()); QMessageBox::warning(this, "Error - QGit", text); QApplication::restoreOverrideCursor(); } break; case MSG_EV: statusBar()->showMessage(data); break; case POPUP_LIST_EV: doContexPopup(data); break; case POPUP_FILE_EV: case POPUP_TREE_EV: doFileContexPopup(data, e->type()); break; default: dbp("ASSERT in MainImpl::event unhandled event %1", e->type()); ret = false; break; } return ret; } int MainImpl::currentTabType(Domain** t) { *t = NULL; QWidget* curPage = tabWdg->currentWidget(); if (curPage == rv->tabPage()) { *t = rv; return TAB_REV; } QList* l = getTabs(curPage); if (l->count() > 0) { *t = l->first(); delete l; return TAB_PATCH; } delete l; QList* l2 = getTabs(curPage); if (l2->count() > 0) { *t = l2->first(); delete l2; return TAB_FILE; } if (l2->count() > 0) dbs("ASSERT in tabType file not found"); delete l2; return -1; } template QList* MainImpl::getTabs(QWidget* tabPage) { QList l = this->findChildren(); QList* ret = new QList; for (int i = 0; i < l.size(); ++i) { if (!tabPage || l.at(i)->tabPage() == tabPage) ret->append(l.at(i)); } return ret; // 'ret' must be deleted by caller } template X* MainImpl::firstTab(QWidget* startPage) { int minVal = 99, firstVal = 99; int startPos = tabWdg->indexOf(startPage); X* min = NULL; X* first = NULL; QList* l = getTabs(); for (int i = 0; i < l->size(); ++i) { X* d = l->at(i); int idx = tabWdg->indexOf(d->tabPage()); if (idx < minVal) { minVal = idx; min = d; } if (idx < firstVal && idx > startPos) { firstVal = idx; first = d; } } delete l; return (first ? first : min); } void MainImpl::tabWdg_currentChanged(int w) { if (w == -1) return; // set correct focus for keyboard browsing Domain* t; switch (currentTabType(&t)) { case TAB_REV: static_cast(t)->tab()->listViewLog->setFocus(); emit closeTabButtonEnabled(false); break; case TAB_PATCH: static_cast(t)->tab()->textEditDiff->setFocus(); emit closeTabButtonEnabled(true); break; case TAB_FILE: static_cast(t)->tab()->histListView->setFocus(); emit closeTabButtonEnabled(true); break; default: dbs("ASSERT in tabWdg_currentChanged: unknown current page"); break; } } void MainImpl::setupShortcuts() { new QShortcut(Qt::Key_I, this, SLOT(shortCutActivated())); new QShortcut(Qt::Key_K, this, SLOT(shortCutActivated())); new QShortcut(Qt::Key_N, this, SLOT(shortCutActivated())); new QShortcut(Qt::Key_Left, this, SLOT(shortCutActivated())); new QShortcut(Qt::Key_Right, this, SLOT(shortCutActivated())); new QShortcut(Qt::Key_Delete, this, SLOT(shortCutActivated())); new QShortcut(Qt::Key_Backspace, this, SLOT(shortCutActivated())); new QShortcut(Qt::Key_Space, this, SLOT(shortCutActivated())); new QShortcut(Qt::Key_B, this, SLOT(shortCutActivated())); new QShortcut(Qt::Key_D, this, SLOT(shortCutActivated())); new QShortcut(Qt::Key_F, this, SLOT(shortCutActivated())); new QShortcut(Qt::Key_P, this, SLOT(shortCutActivated())); new QShortcut(Qt::Key_R, this, SLOT(shortCutActivated())); new QShortcut(Qt::Key_U, this, SLOT(shortCutActivated())); new QShortcut(Qt::SHIFT | Qt::Key_Up, this, SLOT(shortCutActivated())); new QShortcut(Qt::SHIFT | Qt::Key_Down, this, SLOT(shortCutActivated())); new QShortcut(Qt::CTRL | Qt::Key_Plus, this, SLOT(shortCutActivated())); new QShortcut(Qt::CTRL | Qt::Key_Minus, this, SLOT(shortCutActivated())); } void MainImpl::shortCutActivated() { QShortcut* se = dynamic_cast(sender()); if (se) { #if QT_VERSION >= 0x050000 const QKeySequence& key = se->key(); #else const int key = se->key(); #endif if (key == Qt::Key_I) { rv->tab()->listViewLog->on_keyUp(); } else if (key == (Qt::Key_K or key == Qt::Key_N)) { rv->tab()->listViewLog->on_keyDown(); } else if (key == (Qt::SHIFT | Qt::Key_Up)) { goMatch(-1); } else if (key == (Qt::SHIFT | Qt::Key_Down)) { goMatch(1); } else if (key == Qt::Key_Left) { ActBack_activated(); } else if (key == Qt::Key_Right) { ActForward_activated(); } else if (key == (Qt::CTRL | Qt::Key_Plus)) { adjustFontSize(1); //TODO replace magic constant } else if (key == (Qt::CTRL | Qt::Key_Minus)) { adjustFontSize(-1); //TODO replace magic constant } else if (key == Qt::Key_U) { scrollTextEdit(-18); //TODO replace magic constant } else if (key == Qt::Key_D) { scrollTextEdit(18); //TODO replace magic constant } else if (key == Qt::Key_Delete or key == Qt::Key_B or key == Qt::Key_Backspace) { scrollTextEdit(-1); //TODO replace magic constant } else if (key == Qt::Key_Space) { scrollTextEdit(1); } else if (key == Qt::Key_R) { tabWdg->setCurrentWidget(rv->tabPage()); } else if (key == Qt::Key_P or key == Qt::Key_F) { QWidget* cp = tabWdg->currentWidget(); Domain* d = (key == Qt::Key_P) ? static_cast(firstTab(cp)) : static_cast(firstTab(cp)); if (d) tabWdg->setCurrentWidget(d->tabPage()); } } } void MainImpl::goMatch(int delta) { if (ActSearchAndHighlight->isChecked()) rv->tab()->listViewLog->scrollToNextHighlighted(delta); } QTextEdit* MainImpl::getCurrentTextEdit() { QTextEdit* te = NULL; Domain* t; switch (currentTabType(&t)) { case TAB_REV: te = static_cast(t)->tab()->textBrowserDesc; if (!te->isVisible()) te = static_cast(t)->tab()->textEditDiff; break; case TAB_PATCH: te = static_cast(t)->tab()->textEditDiff; break; case TAB_FILE: te = static_cast(t)->tab()->textEditFile; break; default: break; } return te; } void MainImpl::scrollTextEdit(int delta) { QTextEdit* te = getCurrentTextEdit(); if (!te) return; QScrollBar* vs = te->verticalScrollBar(); if (delta == 1 || delta == -1) vs->setValue(vs->value() + delta * (vs->pageStep() - vs->singleStep())); else vs->setValue(vs->value() + delta * vs->singleStep()); } void MainImpl::adjustFontSize(int delta) { // font size is changed on a 'per instance' base and only on list views int ps = QGit::STD_FONT.pointSize() + delta; if (ps < 2) return; QGit::STD_FONT.setPointSize(ps); QSettings settings; settings.setValue(QGit::STD_FNT_KEY, QGit::STD_FONT.toString()); emit changeFont(QGit::STD_FONT); } void MainImpl::fileNamesLoad(int status, int value) { switch (status) { case 1: // stop pbFileNamesLoading->hide(); break; case 2: // update pbFileNamesLoading->setValue(value); break; case 3: // start if (value > 200) { // don't show for few revisions pbFileNamesLoading->reset(); pbFileNamesLoading->setMaximum(value); pbFileNamesLoading->show(); } break; } } // ****************************** Menu ********************************* void MainImpl::updateCommitMenu(bool isStGITStack) { ActCommit->setText(isStGITStack ? "Commit St&GIT patch..." : "&Commit..."); ActAmend->setText(isStGITStack ? "Refresh St&GIT patch..." : "&Amend commit..."); } void MainImpl::updateRecentRepoMenu(SCRef newEntry) { // update menu of all windows foreach (QWidget* widget, QApplication::topLevelWidgets()) { MainImpl* w = dynamic_cast(widget); if (w) w->doUpdateRecentRepoMenu(newEntry); } } void MainImpl::doUpdateRecentRepoMenu(SCRef newEntry) { QList al(File->actions()); FOREACH (QList, it, al) { if ((*it)->data().toString().startsWith("RECENT")) File->removeAction(*it); } QSettings settings; QStringList recents(settings.value(REC_REP_KEY).toStringList()); int idx = recents.indexOf(newEntry); if (idx != -1) recents.removeAt(idx); if (!newEntry.isEmpty()) recents.prepend(newEntry); idx = 1; QStringList newRecents; FOREACH_SL (it, recents) { QAction* newAction = File->addAction(QString::number(idx++) + " " + *it); newAction->setData(QString("RECENT ") + *it); newRecents << *it; if (idx > MAX_RECENT_REPOS) break; } settings.setValue(REC_REP_KEY, newRecents); } static int cntMenuEntries(const QMenu& menu) { int cnt = 0; QList al(menu.actions()); FOREACH (QList, it, al) { if (!(*it)->isSeparator()) cnt++; } return cnt; } void MainImpl::doContexPopup(SCRef sha) { QMenu contextMenu(this); QMenu contextBrnMenu("More branches...", this); QMenu contextTagMenu("More tags...", this); QMenu contextRmtMenu("Remote branches...", this); connect(&contextMenu, SIGNAL(triggered(QAction*)), this, SLOT(goRef_triggered(QAction*))); Domain* t; int tt = currentTabType(&t); bool isRevPage = (tt == TAB_REV); bool isPatchPage = (tt == TAB_PATCH); bool isFilePage = (tt == TAB_FILE); if (isFilePage && ActViewRev->isEnabled()) contextMenu.addAction(ActViewRev); if (!isPatchPage && ActViewDiff->isEnabled()) contextMenu.addAction(ActViewDiff); if (isRevPage && ActViewDiffNewTab->isEnabled()) contextMenu.addAction(ActViewDiffNewTab); if (!isFilePage && ActExternalDiff->isEnabled()) contextMenu.addAction(ActExternalDiff); if (isFilePage && ActExternalEditor->isEnabled()) contextMenu.addAction(ActExternalEditor); if (isRevPage) { updateRevVariables(sha); if (ActCommit->isEnabled() && (sha == ZERO_SHA)) contextMenu.addAction(ActCommit); if (ActCheckout->isEnabled()) contextMenu.addAction(ActCheckout); if (ActBranch->isEnabled()) contextMenu.addAction(ActBranch); if (ActTag->isEnabled()) contextMenu.addAction(ActTag); if (ActDelete->isEnabled()) contextMenu.addAction(ActDelete); if (ActMailFormatPatch->isEnabled()) contextMenu.addAction(ActMailFormatPatch); if (ActPush->isEnabled()) contextMenu.addAction(ActPush); if (ActPop->isEnabled()) contextMenu.addAction(ActPop); const QStringList& bn(git->getAllRefNames(Git::BRANCH, Git::optOnlyLoaded)); const QStringList& rbn(git->getAllRefNames(Git::RMT_BRANCH, Git::optOnlyLoaded)); const QStringList& tn(git->getAllRefNames(Git::TAG, Git::optOnlyLoaded)); QAction* act = NULL; FOREACH_SL (it, rbn) { act = contextRmtMenu.addAction(*it); act->setData("Ref"); } // halve the possible remaining entries for branches and tags int remainingEntries = (MAX_MENU_ENTRIES - cntMenuEntries(contextMenu)); if (!contextRmtMenu.isEmpty()) --remainingEntries; int tagEntries = remainingEntries / 2; int brnEntries = remainingEntries - tagEntries; // display more branches, if there are few tags if (tagEntries > tn.count()) tagEntries = tn.count(); // one branch less because of the "More branches..." submenu if ((bn.count() > brnEntries) && tagEntries) tagEntries++; if (!bn.empty()) contextMenu.addSeparator(); FOREACH_SL (it, bn) { if ( cntMenuEntries(contextMenu) < MAX_MENU_ENTRIES - tagEntries || (*it == bn.last() && contextBrnMenu.isEmpty())) act = contextMenu.addAction(*it); else act = contextBrnMenu.addAction(*it); act->setData("Ref"); } if (!contextBrnMenu.isEmpty()) contextMenu.addMenu(&contextBrnMenu); if (!contextRmtMenu.isEmpty()) contextMenu.addMenu(&contextRmtMenu); if (!tn.empty()) contextMenu.addSeparator(); FOREACH_SL (it, tn) { if ( cntMenuEntries(contextMenu) < MAX_MENU_ENTRIES || (*it == tn.last() && contextTagMenu.isEmpty())) act = contextMenu.addAction(*it); else act = contextTagMenu.addAction(*it); act->setData("Ref"); } if (!contextTagMenu.isEmpty()) contextMenu.addMenu(&contextTagMenu); } QPoint p = QCursor::pos(); p += QPoint(10, 10); contextMenu.exec(p); // remove selected ref name after showing the popup revision_variables.remove(SELECTED_NAME); } void MainImpl::doFileContexPopup(SCRef fileName, int type) { QMenu contextMenu(this); Domain* t; int tt = currentTabType(&t); bool isRevPage = (tt == TAB_REV); bool isPatchPage = (tt == TAB_PATCH); bool isDir = treeView->isDir(fileName); if (type == POPUP_FILE_EV) if (!isPatchPage && ActViewDiff->isEnabled()) contextMenu.addAction(ActViewDiff); if (!isDir && ActViewFile->isEnabled()) contextMenu.addAction(ActViewFile); if (!isDir && ActViewFileNewTab->isEnabled()) contextMenu.addAction(ActViewFileNewTab); if (!isRevPage && (type == POPUP_FILE_EV) && ActViewRev->isEnabled()) contextMenu.addAction(ActViewRev); if (ActFilterTree->isEnabled()) contextMenu.addAction(ActFilterTree); if (!isDir) { if (ActSaveFile->isEnabled()) contextMenu.addAction(ActSaveFile); if ((type == POPUP_FILE_EV) && ActExternalDiff->isEnabled()) contextMenu.addAction(ActExternalDiff); if ((type == POPUP_FILE_EV) && ActExternalEditor->isEnabled()) contextMenu.addAction(ActExternalEditor); if (ActExternalEditor->isEnabled()) contextMenu.addAction(ActExternalEditor); } contextMenu.exec(QCursor::pos()); } void MainImpl::goRef_triggered(QAction* act) { if (!act || act->data() != "Ref") return; SCRef refSha(git->getRefSha(act->text())); rv->st.setSha(refSha); UPDATE_DOMAIN(rv); } void MainImpl::ActSplitView_activated() { Domain* t; switch (currentTabType(&t)) { case TAB_REV: { RevsView* rv = static_cast(t); QWidget* w = rv->tab()->fileList; QSplitter* sp = static_cast(w->parent()); sp->setHidden(w->isVisible()); } break; case TAB_PATCH: { PatchView* pv = static_cast(t); QWidget* w = pv->tab()->textBrowserDesc; w->setHidden(w->isVisible()); } break; case TAB_FILE: { FileView* fv = static_cast(t); QWidget* w = fv->tab()->histListView; w->setHidden(w->isVisible()); } break; default: dbs("ASSERT in ActSplitView_activated: unknown current page"); break; } } void MainImpl::ActToggleLogsDiff_activated() { Domain* t; if (currentTabType(&t) == TAB_REV) { RevsView* rv = static_cast(t); rv->toggleDiffView(); } } const QString MainImpl::getRevisionDesc(SCRef sha) { bool showHeader = ActShowDescHeader->isChecked(); return git->getDesc(sha, shortLogRE, longLogRE, showHeader, NULL); } void MainImpl::ActShowDescHeader_activated() { // each open tab get his description, // could be different for each tab emit updateRevDesc(); } void MainImpl::ActShowTree_toggled(bool b) { if (b) { treeView->show(); UPDATE_DOMAIN(rv); } else { saveCurrentGeometry(); treeView->hide(); } } void MainImpl::ActSaveFile_activated() { QFileInfo f(rv->st.fileName()); const QString fileName(QFileDialog::getSaveFileName(this, "Save file as", f.fileName())); if (fileName.isEmpty()) return; QApplication::setOverrideCursor(QCursor(Qt::WaitCursor)); QString fileSha(git->getFileSha(rv->st.fileName(), rv->st.sha())); if (!git->saveFile(fileSha, rv->st.fileName(), fileName)) statusBar()->showMessage("Unable to save " + fileName); QApplication::restoreOverrideCursor(); } void MainImpl::openRecent_triggered(QAction* act) { const QString dataString = act->data().toString(); if (!dataString.startsWith("RECENT")) // only recent repos entries have "RECENT" in data field return; const QString workDir = dataString.mid(7); if (!workDir.isEmpty()) { QDir d(workDir); if (d.exists()) setRepository(workDir); else statusBar()->showMessage("Directory '" + workDir + "' does not seem to exist anymore"); } } void MainImpl::ActOpenRepo_activated() { const QString dirName(QFileDialog::getExistingDirectory(this, "Choose a directory", curDir)); if (!dirName.isEmpty()) { QDir d(dirName); setRepository(d.absolutePath()); } } void MainImpl::ActOpenRepoNewWindow_activated() { const QString dirName(QFileDialog::getExistingDirectory(this, "Choose a directory", curDir)); if (!dirName.isEmpty()) { QDir d(dirName); MainImpl* newWin = new MainImpl(d.absolutePath()); newWin->show(); } } void MainImpl::refreshRepo(bool b) { setRepository(curDir, true, b); } void MainImpl::ActRefresh_activated() { refreshRepo(true); } void MainImpl::ActMailFormatPatch_activated() { QStringList selectedItems; rv->tab()->listViewLog->getSelectedItems(selectedItems); if (selectedItems.isEmpty()) { statusBar()->showMessage("At least one selected revision needed"); return; } if (selectedItems.contains(ZERO_SHA)) { statusBar()->showMessage("Unable to save a patch for not committed content"); return; } QSettings settings; QString outDir(settings.value(PATCH_DIR_KEY, curDir).toString()); QString dirPath(QFileDialog::getExistingDirectory(this, "Choose destination directory - Save Patch", outDir)); if (dirPath.isEmpty()) return; QDir d(dirPath); settings.setValue(PATCH_DIR_KEY, d.absolutePath()); QApplication::setOverrideCursor(QCursor(Qt::WaitCursor)); git->formatPatch(selectedItems, d.absolutePath()); QApplication::restoreOverrideCursor(); } bool MainImpl::askApplyPatchParameters(bool* workDirOnly, bool* fold) { int ret = 0; if (!git->isStGITStack()) { ret = QMessageBox::question(this, "Apply Patch", "Do you want to commit or just to apply changes to " "working directory?", "&Cancel", "&Working directory", "&Commit", 0, 0); *workDirOnly = (ret == 1); *fold = false; } else { ret = QMessageBox::question(this, "Apply Patch", "Do you want to " "import or fold the patch?", "&Cancel", "&Fold", "&Import", 0, 0); *workDirOnly = false; *fold = (ret == 1); } return (ret != 0); } void MainImpl::ActMailApplyPatch_activated() { QSettings settings; QString outDir(settings.value(PATCH_DIR_KEY, curDir).toString()); QString patchName(QFileDialog::getOpenFileName(this, "Choose the patch file - Apply Patch", outDir, "Patches (*.patch *.diff *.eml)\nAll Files (*.*)")); if (patchName.isEmpty()) return; QFileInfo f(patchName); settings.setValue(PATCH_DIR_KEY, f.absolutePath()); bool workDirOnly, fold; if (!askApplyPatchParameters(&workDirOnly, &fold)) return; QApplication::setOverrideCursor(QCursor(Qt::WaitCursor)); bool ok = git->applyPatchFile(f.absoluteFilePath(), fold, !Git::optDragDrop); if (workDirOnly && ok) git->resetCommits(1); QApplication::restoreOverrideCursor(); refreshRepo(false); } void MainImpl::ActCheckWorkDir_toggled(bool b) { if (!ActCheckWorkDir->isEnabled()) // to avoid looping with setChecked() return; setFlag(DIFF_INDEX_F, b); bool keepSelection = (rv->st.sha() != ZERO_SHA); refreshRepo(keepSelection); } void MainImpl::ActSettings_activated() { SettingsImpl setView(this, git); connect(&setView, SIGNAL(typeWriterFontChanged()), this, SIGNAL(typeWriterFontChanged())); connect(&setView, SIGNAL(flagChanged(uint)), this, SIGNAL(flagChanged(uint))); setView.exec(); // update ActCheckWorkDir if necessary if (ActCheckWorkDir->isChecked() != testFlag(DIFF_INDEX_F)) ActCheckWorkDir->toggle(); } void MainImpl::ActCustomActionSetup_activated() { CustomActionImpl* ca = new CustomActionImpl(); // has Qt::WA_DeleteOnClose connect(this, SIGNAL(closeAllWindows()), ca, SLOT(close())); connect(ca, SIGNAL(listChanged(const QStringList&)), this, SLOT(customActionListChanged(const QStringList&))); ca->show(); } void MainImpl::customActionListChanged(const QStringList& list) { // update menu of all windows foreach (QWidget* widget, QApplication::topLevelWidgets()) { MainImpl* w = dynamic_cast(widget); if (w) w->doUpdateCustomActionMenu(list); } } void MainImpl::doUpdateCustomActionMenu(const QStringList& list) { QAction* setupAct = Actions->actions().first(); // is never empty Actions->removeAction(setupAct); Actions->clear(); Actions->addAction(setupAct); if (list.isEmpty()) return; Actions->addSeparator(); FOREACH_SL (it, list) Actions->addAction(*it); } void MainImpl::customAction_triggered(QAction* act) { SCRef actionName = act->text(); if (actionName == "Setup actions...") return; QSettings set; if (!set.value(ACT_LIST_KEY).toStringList().contains(actionName)) { dbp("ASSERT in customAction_activated, action %1 not found", actionName); return; } QString cmd = set.value(ACT_GROUP_KEY + actionName + ACT_TEXT_KEY).toString().trimmed(); if (testFlag(ACT_CMD_LINE_F, ACT_GROUP_KEY + actionName + ACT_FLAGS_KEY)) { // for backwards compatibility: if ACT_CMD_LINE_F is set, insert a dialog token in first line int pos = cmd.indexOf('\n'); if (pos < 0) pos = cmd.length(); cmd.insert(pos, " %lineedit:cmdline args%"); } updateRevVariables(lineEditSHA->text()); InputDialog dlg(cmd, revision_variables, "Run custom action: " + actionName, this); if (!dlg.empty() && dlg.exec() != QDialog::Accepted) return; try { cmd = dlg.replace(revision_variables); // replace variables } catch (const std::exception &e) { QMessageBox::warning(this, "Custom action command", e.what()); return; } if (cmd.isEmpty()) return; ConsoleImpl* c = new ConsoleImpl(actionName, git); // has Qt::WA_DeleteOnClose attribute connect(this, SIGNAL(typeWriterFontChanged()), c, SLOT(typeWriterFontChanged())); connect(this, SIGNAL(closeAllWindows()), c, SLOT(close())); connect(c, SIGNAL(customAction_exited(const QString&)), this, SLOT(customAction_exited(const QString&))); if (c->start(cmd)) c->show(); } void MainImpl::customAction_exited(const QString& name) { const QString flags(ACT_GROUP_KEY + name + ACT_FLAGS_KEY); if (testFlag(ACT_REFRESH_F, flags)) QTimer::singleShot(10, this, SLOT(refreshRepo())); // outside of event handler } void MainImpl::ActCommit_activated() { CommitImpl* c = new CommitImpl(git, false); // has Qt::WA_DeleteOnClose attribute connect(this, SIGNAL(closeAllWindows()), c, SLOT(close())); connect(c, SIGNAL(changesCommitted(bool)), this, SLOT(changesCommitted(bool))); c->show(); } void MainImpl::ActAmend_activated() { CommitImpl* c = new CommitImpl(git, true); // has Qt::WA_DeleteOnClose attribute connect(this, SIGNAL(closeAllWindows()), c, SLOT(close())); connect(c, SIGNAL(changesCommitted(bool)), this, SLOT(changesCommitted(bool))); c->show(); } void MainImpl::changesCommitted(bool ok) { if (ok) refreshRepo(false); else statusBar()->showMessage("Failed to commit changes"); } void MainImpl::ActCommit_setEnabled(bool b) { // pop and push commands fail if there are local changes, // so in this case we disable ActPop and ActPush if (b) { ActPush->setEnabled(false); ActPop->setEnabled(false); } ActCommit->setEnabled(b); } /** Checkout supports various operation modes: * - switching to an existing branch (standard use case) * - create and checkout a new branch * - resetting an existing branch to a new sha */ void MainImpl::ActCheckout_activated() { QString sha = lineEditSHA->text(), rev = sha; const QString branchKey("local branch name"); QString cmd = "git checkout -q "; const QString &selected_name = revision_variables.value(SELECTED_NAME).toString(); const QString ¤t_branch = revision_variables.value(CURRENT_BRANCH).toString(); const QStringList &local_branches = revision_variables.value(REV_LOCAL_BRANCHES).toStringList(); if (!selected_name.isEmpty() && local_branches.contains(selected_name) > 0 && selected_name != current_branch) { // standard branch switching: directly checkout selected branch rev = selected_name; } else { // ask for (new) local branch name QString title = QString("Checkout "); if (selected_name.isEmpty()) { title += QString("revision ") + sha.mid(0, 8); } else { title += QString("branch ") + selected_name; rev = selected_name; } // merge all reference names into a single list const QStringList &rmts = revision_variables.value(REV_REMOTE_BRANCHES).toStringList(); QStringList all_names; all_names << revision_variables.value(REV_LOCAL_BRANCHES).toStringList(); for(QStringList::const_iterator it=rmts.begin(), end=rmts.end(); it!=end; ++it) { // drop initial / from name int pos = it->indexOf('/'); if (pos < 0) continue; all_names << it->mid(pos+1); } revision_variables.insert("ALL_NAMES", all_names); InputDialog dlg(QString("%combobox[editable,ref,empty]:%1=$ALL_NAMES%").arg(branchKey), revision_variables, title, this); if (dlg.exec() != QDialog::Accepted) return; QString branch = dlg.value(branchKey).toString(); if (!branch.isEmpty()) { SCRef refsha = git->getRefSha(branch, Git::BRANCH, true); if (refsha == sha) rev = branch; // checkout existing branch, even if name wasn't directly selected else if (!refsha.isEmpty()) { if (QMessageBox::warning(this, "Checkout " + branch, QString("Branch %1 already exists. Reset?").arg(branch), QMessageBox::Yes | QMessageBox::No, QMessageBox::No) != QMessageBox::Yes) return; else cmd.append("-B ").append(branch); // reset an existing branch } else { cmd.append("-b ").append(branch); // create new local branch } } // if new branch name is empty, checkout detached } cmd.append(" ").append(rev); QApplication::setOverrideCursor(QCursor(Qt::WaitCursor)); if (!git->run(cmd)) statusBar()->showMessage("Failed to checkout " + rev); refreshRepo(true); QApplication::restoreOverrideCursor(); } void MainImpl::ActBranch_activated() { doBranchOrTag(false); } void MainImpl::ActTag_activated() { doBranchOrTag(true); } const QStringList& stripNames(QStringList& names) { for(QStringList::iterator it=names.begin(), end=names.end(); it!=end; ++it) *it = it->section('/', -1); return names; } void MainImpl::doBranchOrTag(bool isTag) { const QString sha = lineEditSHA->text(); QString refDesc = isTag ? "tag" : "branch"; QString dlgTitle = "Create " + refDesc + " - QGit"; QString dlgDesc = "%lineedit[ref]:name=$ALL_NAMES%"; InputDialog::VariableMap dlgVars; QStringList allNames = git->getAllRefNames(Git::BRANCH | Git::RMT_BRANCH | Git::TAG, false); stripNames(allNames); allNames.removeDuplicates(); allNames.sort(); dlgVars.insert("ALL_NAMES", allNames); if (isTag) { QString revDesc(rv->tab()->listViewLog->currentText(LOG_COL)); dlgDesc += "%textedit:message=$MESSAGE%"; dlgVars.insert("MESSAGE", revDesc); } InputDialog dlg(dlgDesc, dlgVars, dlgTitle, this); if (dlg.exec() != QDialog::Accepted) return; const QString& ref = dlg.value("name").toString(); bool force = false; if (!git->getRefSha(ref, isTag ? Git::TAG : Git::BRANCH, false).isEmpty()) { if (QMessageBox::warning(this, dlgTitle, refDesc + " name '" + ref + "' already exists.\n" "Force reset?", QMessageBox::Yes | QMessageBox::No, QMessageBox::No) != QMessageBox::Yes) return; force = true; } QApplication::setOverrideCursor(QCursor(Qt::WaitCursor)); QString cmd; if (isTag) { const QString& msg = dlg.value("message").toString(); cmd = "git tag "; if (!msg.isEmpty()) cmd += "-m \"" + msg + "\" "; } else { cmd = "git branch "; } if (force) cmd += "-f "; cmd += ref + " " + sha; if (git->run(cmd)) refreshRepo(true); else statusBar()->showMessage("Failed to create " + refDesc + " " + ref); QApplication::restoreOverrideCursor(); } // put a ref name into a corresponding StringList for tags, remotes, and local branches typedef QMap RefGroupMap; static void groupRef(const QString& ref, RefGroupMap& groups) { QString group, name; if (ref.startsWith("tags/")) { group = ref.left(5); name = ref.mid(5); } else if (ref.startsWith("remotes/")) { group = ref.section('/', 1, 1); name = ref.section('/', 2); } else { group = ""; name = ref; } if (!groups.contains(group)) groups.insert(group, QStringList()); QStringList &l = groups[group]; l << name; } void MainImpl::ActDelete_activated() { const QString &selected_name = revision_variables.value(SELECTED_NAME).toString(); const QStringList &tags = revision_variables.value(REV_TAGS).toStringList(); const QStringList &rmts = revision_variables.value(REV_REMOTE_BRANCHES).toStringList(); // merge all reference names into a single list QStringList all_names; all_names << revision_variables.value(REV_LOCAL_BRANCHES).toStringList(); for (QStringList::const_iterator it=rmts.begin(), end=rmts.end(); it!=end; ++it) all_names << "remotes/" + *it; for (QStringList::const_iterator it=tags.begin(), end=tags.end(); it!=end; ++it) all_names << "tags/" + *it; // group selected names by origin QMap groups; if (!selected_name.isEmpty()) { groupRef(selected_name, groups); } else if (all_names.size() == 1) { groupRef(all_names.first(), groups); } else { revision_variables.insert("ALL_NAMES", all_names); InputDialog dlg("%listbox:_refs=$ALL_NAMES%", revision_variables, "Delete references - QGit", this); QListView *w = dynamic_cast(dlg.widget("_refs")); w->setSelectionMode(QAbstractItemView::ExtendedSelection); if (dlg.exec() != QDialog::Accepted) return; QModelIndexList selected = w->selectionModel()->selectedIndexes(); for (QModelIndexList::const_iterator it=selected.begin(), end=selected.end(); it!=end; ++it) groupRef(it->data().toString(), groups); } if (groups.empty()) return; // group selected names by origin QApplication::setOverrideCursor(QCursor(Qt::WaitCursor)); bool ok = true; for (RefGroupMap::const_iterator g = groups.begin(), gend = groups.end(); g != gend; ++g) { QString cmd; if (g.key() == "") // local branches cmd = "git branch -d " + g.value().join(" "); else if (g.key() == "tags/") // tags cmd = "git tag -d " + g.value().join(" "); else // remote branches cmd = "git push -q " + g.key() + " :" + g.value().join(" :"); ok &= git->run(cmd); } refreshRepo(true); QApplication::restoreOverrideCursor(); if (!ok) statusBar()->showMessage("Failed, to remove some refs."); } void MainImpl::ActPush_activated() { QStringList selectedItems; rv->tab()->listViewLog->getSelectedItems(selectedItems); for (int i = 0; i < selectedItems.count(); i++) { if (!git->checkRef(selectedItems[i], Git::UN_APPLIED)) { statusBar()->showMessage("Please, select only unapplied patches"); return; } } QApplication::setOverrideCursor(QCursor(Qt::WaitCursor)); bool ok = true; for (int i = 0; i < selectedItems.count(); i++) { const QString tmp(QString("Pushing patch %1 of %2") .arg(i+1).arg(selectedItems.count())); statusBar()->showMessage(tmp); SCRef sha = selectedItems[selectedItems.count() - i - 1]; if (!git->stgPush(sha)) { statusBar()->showMessage("Failed to push patch " + sha); ok = false; break; } } if (ok) statusBar()->clearMessage(); QApplication::restoreOverrideCursor(); refreshRepo(false); } void MainImpl::ActPop_activated() { QStringList selectedItems; rv->tab()->listViewLog->getSelectedItems(selectedItems); if (selectedItems.count() > 1) { statusBar()->showMessage("Please, select one revision only"); return; } QApplication::setOverrideCursor(QCursor(Qt::WaitCursor)); git->stgPop(selectedItems[0]); QApplication::restoreOverrideCursor(); refreshRepo(false); } void MainImpl::ActFilterTree_toggled(bool b) { if (!ActFilterTree->isEnabled()) { dbs("ASSERT ActFilterTree_toggled while disabled"); return; } if (b) { QStringList selectedItems; if (!treeView->isVisible()) treeView->updateTree(); // force tree updating treeView->getTreeSelectedItems(selectedItems); if (selectedItems.count() == 0) { dbs("ASSERT tree filter action activated with no selected items"); return; } statusBar()->showMessage("Filter view on " + selectedItems.join(" ")); setRepository(curDir, true, true, &selectedItems); } else refreshRepo(true); } void MainImpl::ActFindNext_activated() { QTextEdit* te = getCurrentTextEdit(); if (!te || textToFind.isEmpty()) return; bool endOfDocument = false; while (true) { if (te->find(textToFind)) return; if (endOfDocument) { QMessageBox::warning(this, "Find text - QGit", "Text \"" + textToFind + "\" not found!", QMessageBox::Ok, 0); return; } if (QMessageBox::question(this, "Find text - QGit", "End of document " "reached\n\nDo you want to continue from beginning?", QMessageBox::Yes, QMessageBox::No | QMessageBox::Escape) == QMessageBox::No) return; endOfDocument = true; te->moveCursor(QTextCursor::Start); } } void MainImpl::ActFind_activated() { QTextEdit* te = getCurrentTextEdit(); if (!te) return; QString def(textToFind); if (te->textCursor().hasSelection()) def = te->textCursor().selectedText().section('\n', 0, 0); else te->moveCursor(QTextCursor::Start); bool ok; QString str(QInputDialog::getText(this, "Find text - QGit", "Text to find:", QLineEdit::Normal, def, &ok)); if (!ok || str.isEmpty()) return; textToFind = str; // update with valid data only ActFindNext_activated(); } void MainImpl::ActHelp_activated() { QDialog* dlg = new QDialog(); dlg->setAttribute(Qt::WA_DeleteOnClose); Ui::HelpBase ui; ui.setupUi(dlg); ui.textEditHelp->setHtml(QString::fromLatin1(helpInfo)); // defined in help.h connect(this, SIGNAL(closeAllWindows()), dlg, SLOT(close())); dlg->show(); dlg->raise(); } void MainImpl::ActMarkDiffToSha_activated() { ListView* lv = rv->tab()->listViewLog; lv->markDiffToSha(lineEditSHA->text()); } void MainImpl::ActAbout_activated() { static const char* aboutMsg = "

QGit version " PACKAGE_VERSION "

" "

Copyright (c) 2005, 2007, 2008 Marco Costalba

" "

Use and redistribute under the terms of the
" "GNU General Public License Version 2

" "

Contributors:
" "Copyright (c) 2007 Andy Parkins
" "Copyright (c) 2007 Pavel Roskin
" "Copyright (c) 2007 Peter Oberndorfer
" "Copyright (c) 2007 Yaacov Akiba
" "Copyright (c) 2007 James McKaskill
" "Copyright (c) 2008 Jan Hudec
" "Copyright (c) 2008 Paul Gideon Dann
" "Copyright (c) 2008 Oliver Bock
" "Copyright (c) 2010 Cyp <cyp561@gmail.com>
" "Copyright (c) 2011 Jean-François Dagenais <dagenaisj@sonatest.com>
" "Copyright (c) 2011 Pavel Tikhomirov <pavtih@gmail.com>
" "Copyright (c) 2011-2016 Cristian Tibirna <tibirna@kde.org>
" "Copyright (c) 2011 Tim Blechmann <tim@klingt.org>
" "Copyright (c) 2014 Gregor Mi <codestruct@posteo.org>
" "Copyright (c) 2014 Sbytov N.N <sbytnn@gmail.com>
" "Copyright (c) 2015 Daniel Levin <dendy.ua@gmail.com>
" "Copyright (c) 2017 Luigi Toscano <luigi.toscano@tiscali.it>
" "Copyright (c) 2016 Pavel Karelin <hkarel@yandex.ru>
" "Copyright (c) 2016 Zane Bitter <zbitter@redhat.com>
" "Copyright (c) 2016 Robert Haschke <rhaschke@techfak.uni-bielefeld.de>
" "Copyright (c) 2017 Andrey Rahmatullin $lt;wrar@wrar.name>" "

" "

This version was compiled against Qt " QT_VERSION_STR "

"; QMessageBox::about(this, "About QGit", QString::fromLatin1(aboutMsg)); } void MainImpl::closeEvent(QCloseEvent* ce) { saveCurrentGeometry(); // lastWindowClosed() signal is emitted by close(), after sending // closeEvent(), so we need to close _here_ all secondary windows before // the close() method checks for lastWindowClosed flag to avoid missing // the signal and stay in the main loop forever, because lastWindowClosed() // signal is connected to qApp->quit() // // note that we cannot rely on setting 'this' parent in secondary windows // because when close() is called children are still alive and, finally, // when children are deleted, d'tor do not call close() anymore. So we miss // lastWindowClosed() signal in this case. emit closeAllWindows(); hide(); EM_RAISE(exExiting); git->stop(Git::optSaveCache); if (!git->findChildren().isEmpty()) { // if not all processes have been deleted, there is // still some run() call not returned somewhere, it is // not safe to delete run() callers objects now QTimer::singleShot(100, this, SLOT(ActClose_activated())); ce->ignore(); return; } emit closeAllTabs(); delete rv; QWidget::closeEvent(ce); } void MainImpl::ActClose_activated() { close(); } void MainImpl::ActExit_activated() { qApp->closeAllWindows(); } qgit-2.7/src/mainimpl.h000066400000000000000000000146451305655150700151150ustar00rootroot00000000000000/* Author: Marco Costalba (C) 2005-2007 Copyright: See COPYING file that comes with this distribution */ #ifndef MAINIMPL_H #define MAINIMPL_H #include #include #include #include "exceptionmanager.h" #include "common.h" #include "ui_mainview.h" class QAction; class QCloseEvent; class QComboBox; class QEvent; class QListWidgetItem; class QModelIndex; class QProgressBar; class QShortcutEvent; class QTextEdit; class Domain; class Git; class FileHistory; class FileView; class RevsView; class MainImpl : public QMainWindow, public Ui_MainBase { Q_OBJECT public: MainImpl(const QString& curDir = "", QWidget* parent = 0); void updateContextActions(SCRef newRevSha, SCRef newFileName, bool isDir, bool found); const QString getRevisionDesc(SCRef sha); // not buildable with Qt designer, will be created manually QLineEdit* lineEditSHA; QLineEdit* lineEditFilter; enum ComboSearch { CS_SHORT_LOG, CS_LOG_MSG, CS_AUTHOR, CS_SHA1, CS_FILE, CS_PATCH, CS_PATCH_REGEXP }; QComboBox* cmbSearch; signals: void highlightPatch(const QString&, bool); void updateRevDesc(); void closeAllWindows(); void closeAllTabs(); void changeFont(const QFont&); void closeTabButtonEnabled(bool); void typeWriterFontChanged(); void flagChanged(uint); private slots: void tabWdg_currentChanged(int); void newRevsAdded(const FileHistory*, const QVector&); void fileNamesLoad(int, int); void revisionsDragged(const QStringList&); void revisionsDropped(const QStringList&); void shortCutActivated(); protected: virtual bool event(QEvent* e); protected slots: void initWithEventLoopActive(); void refreshRepo(bool setCurRevAfterLoad = true); void listViewLog_doubleClicked(const QModelIndex&); void fileList_itemDoubleClicked(QListWidgetItem*); void treeView_doubleClicked(QTreeWidgetItem*, int); void histListView_doubleClicked(const QModelIndex&); void customActionListChanged(const QStringList& list); void openRecent_triggered(QAction*); void customAction_triggered(QAction*); void customAction_exited(const QString& name); void goRef_triggered(QAction*); void changesCommitted(bool); void lineEditSHA_returnPressed(); void lineEditFilter_returnPressed(); void pushButtonCloseTab_clicked(); void ActBack_activated(); void ActForward_activated(); void ActFind_activated(); void ActFindNext_activated(); void ActRangeDlg_activated(); void ActViewRev_activated(); void ActViewFile_activated(); void ActViewFileNewTab_activated(); void ActViewDiff_activated(); void ActViewDiffNewTab_activated(); void ActExternalDiff_activated(); void ActExternalEditor_activated(); void ActSplitView_activated(); void ActToggleLogsDiff_activated(); void ActShowDescHeader_activated(); void ActOpenRepo_activated(); void ActOpenRepoNewWindow_activated(); void ActRefresh_activated(); void ActSaveFile_activated(); void ActMailFormatPatch_activated(); void ActMailApplyPatch_activated(); void ActSettings_activated(); void ActCommit_activated(); void ActAmend_activated(); void ActCheckout_activated(); void ActBranch_activated(); void ActTag_activated(); void ActDelete_activated(); void ActPush_activated(); void ActPop_activated(); void ActClose_activated(); void ActExit_activated(); void ActSearchAndFilter_toggled(bool); void ActSearchAndHighlight_toggled(bool); void ActCustomActionSetup_activated(); void ActCheckWorkDir_toggled(bool); void ActShowTree_toggled(bool); void ActFilterTree_toggled(bool); void ActAbout_activated(); void ActHelp_activated(); void ActMarkDiffToSha_activated(); void closeEvent(QCloseEvent* ce); private: friend class setRepoDelayed; virtual bool eventFilter(QObject* obj, QEvent* ev); void updateGlobalActions(bool b); void updateRevVariables(SCRef sha); void setupShortcuts(); int currentTabType(Domain** t); void filterList(bool isOn, bool onlyHighlight); bool isMatch(SCRef sha, SCRef f, int cn, const QMap& sm); void highlightAbbrevSha(SCRef abbrevSha); void setRepository(SCRef wd, bool = false, bool = false, const QStringList* = NULL, bool = false); void getExternalDiffArgs(QStringList* args, QStringList* filenames); QStringList getExternalEditorArgs(); void lineEditSHASetText(SCRef text); void updateCommitMenu(bool isStGITStack); void updateRecentRepoMenu(SCRef newEntry = ""); void doUpdateRecentRepoMenu(SCRef newEntry); void doUpdateCustomActionMenu(const QStringList& list); void doBranchOrTag(bool isTag); void ActCommit_setEnabled(bool b); void doContexPopup(SCRef sha); void doFileContexPopup(SCRef fileName, int type); void adjustFontSize(int delta); void scrollTextEdit(int delta); void goMatch(int delta); bool askApplyPatchParameters(bool* commit, bool* fold); void saveCurrentGeometry(); QTextEdit* getCurrentTextEdit(); template QList* getTabs(QWidget* tabPage = NULL); template X* firstTab(QWidget* startPage = NULL); void openFileTab(FileView* fv = NULL); EM_DECLARE(exExiting); Git* git; RevsView* rv; QProgressBar* pbFileNamesLoading; // curDir is the repository working directory, could be different from qgit running // directory QDir::current(). Note that qgit could be run from subdirectory // so only after git->isArchive() that updates curDir to point to working directory // we are sure is correct. QString curDir; QString startUpDir; QString textToFind; QRegExp shortLogRE; QRegExp longLogRE; QMap revision_variables; // variables used in generic input dialogs bool setRepositoryBusy; }; class ExternalDiffProc : public QProcess { Q_OBJECT public: ExternalDiffProc(const QStringList& f, QObject* p) : QProcess(p), filenames(f) { connect(this, SIGNAL(finished(int, QProcess::ExitStatus)), this, SLOT(on_finished(int, QProcess::ExitStatus))); } ~ExternalDiffProc() { terminate(); removeFiles(); } QStringList filenames; private slots: void on_finished(int, QProcess::ExitStatus) { deleteLater(); } private: void removeFiles() { if (!filenames.empty()) { QDir d; // remove temporary files to diff on d.remove(filenames[0]); d.remove(filenames[1]); } } }; class ExternalEditorProc : public QProcess { Q_OBJECT public: ExternalEditorProc(QObject* p) : QProcess(p) { connect(this, SIGNAL(finished(int, QProcess::ExitStatus)), this, SLOT(on_finished(int, QProcess::ExitStatus))); } ~ExternalEditorProc() { terminate(); } private slots: void on_finished(int, QProcess::ExitStatus) { deleteLater(); } private: }; #endif qgit-2.7/src/mainview.ui000066400000000000000000001120341305655150700153030ustar00rootroot00000000000000 MainBase 0 0 905 477 true QGit :/icons/resources/qgit.png:/icons/resources/qgit.png 6 9 9 9 9 Qt::Horizontal Qt::CustomContextMenu QAbstractItemView::ExtendedSelection false 1 Git tree Tab&Wdg Toolbar Qt::Horizontal 16 16 TopToolBarArea false 0 0 905 20 &Help &File true &Edit &Actions &View :/icons/resources/folder_open.png:/icons/resources/folder_open.png &Open... Open Ctrl+O E&xit Exit Ctrl+Q &About QGit About false :/icons/resources/mail_send.png:/icons/resources/mail_send.png &Save patch... Save patch Save a patch series to be sent by e-mail :/icons/resources/help.png:/icons/resources/help.png QGit ha&ndbook Handbook F1 :/icons/resources/configure.png:/icons/resources/configure.png &Settings... Settings false :/icons/resources/vcs_commit.png:/icons/resources/vcs_commit.png &Commit... Commit Commit changes # false Make &tag... Make tag Tag selected revision Ctrl+T Open in a &new window... Open in a &new window Ctrl+N &Close Close Ctrl+W false :/icons/resources/mail_get.png:/icons/resources/mail_get.png Apply patch... Apply a patch series false &Push patch Push patch Push this patch on StGIT stack PgUp false P&op patch Pop patch Pop patches from StGIT stack until this one included PgDown false :/icons/resources/reload.png:/icons/resources/reload.png Refresh Refresh view (F5) F5 false View patch View patch Double click on revision log as a shortcut Double click on revision log as a shortcut Ctrl+P false View file View file Double click on file name as a shortcut Double click on file name as a shortcut Ctrl+A false Save file as... Save file as... Save selected file Ctrl+S true false :/icons/resources/wizard.png:/icons/resources/wizard.png Toggle filter by tree Filter by tree Filter by tree selection, multiple selections allowed true false :/icons/resources/view_tree.png:/icons/resources/view_tree.png Toggle tree view Archive tree Toggle tree view (T) T false Delete references... Delete tag Remove selected reference names true false Check working directory Check working dir Check working directory Mark for diff Mark for diff Mark selected revision to diff against false External diff... Launch external diff viewer Ctrl+D false External editor... Launch external editor Ctrl+E false View revision View revision Double click on a revision in file history as a shortcut Ctrl+R :/icons/resources/view_top_bottom.png:/icons/resources/view_top_bottom.png Toggle split view Toggle split view Hide/unhide secondary pane S Find... Find text in current view... Ctrl+F Find next Find next Go to next occurrence of searched text F3 false :/icons/resources/next.png:/icons/resources/next.png Forward Forward Alt+Right false :/icons/resources/previous.png:/icons/resources/previous.png Back Back Alt+Left :/icons/resources/bookmark.png:/icons/resources/bookmark.png Setup actions... Setup actions... false View file in new tab View file in new tab Ctrl+Shift+A false View patch in new tab View patch in new tab Ctrl+Shift+P true true :/icons/resources/filter.png:/icons/resources/filter.png Search and filter Toggle filter true true :/icons/resources/find.png:/icons/resources/find.png Search and highlight Toggle match highlight true true Show header info Show header info Show revision description header info H Toggle message/diff Toggle message/diff Toggle between messages and patch content\n in revision description pane M :/icons/resources/range_select.png:/icons/resources/range_select.png Select range... Show 'Select range' dialog Amend commit... Amend previous commit @ false Make &branch... Make new branch on selected revision Ctrl+B &Checkout... Checkout Checkout selected revision Ctrl+C TreeView QTreeWidget
treeview.h
ActAbout triggered() MainBase ActAbout_activated() -1 -1 20 20 ActCheckWorkDir toggled(bool) MainBase ActCheckWorkDir_toggled(bool) -1 -1 20 20 ActCheckout triggered() MainBase ActCheckout_activated() -1 -1 20 20 ActClose triggered() MainBase ActClose_activated() -1 -1 20 20 ActCommit triggered() MainBase ActCommit_activated() -1 -1 20 20 ActExit triggered() MainBase ActExit_activated() -1 -1 20 20 ActExternalDiff triggered() MainBase ActExternalDiff_activated() -1 -1 20 20 ActExternalEditor triggered() MainBase ActExternalEditor_activated() -1 -1 20 20 ActFilterTree toggled(bool) MainBase ActFilterTree_toggled(bool) -1 -1 20 20 ActHelp triggered() MainBase ActHelp_activated() -1 -1 20 20 ActMailApplyPatch triggered() MainBase ActMailApplyPatch_activated() -1 -1 20 20 ActMailFormatPatch triggered() MainBase ActMailFormatPatch_activated() -1 -1 20 20 ActOpenRepo triggered() MainBase ActOpenRepo_activated() -1 -1 20 20 ActOpenRepoNewWindow triggered() MainBase ActOpenRepoNewWindow_activated() -1 -1 20 20 ActPop triggered() MainBase ActPop_activated() -1 -1 20 20 ActPush triggered() MainBase ActPush_activated() -1 -1 20 20 ActRefresh triggered() MainBase ActRefresh_activated() -1 -1 20 20 ActSaveFile triggered() MainBase ActSaveFile_activated() -1 -1 20 20 ActSettings triggered() MainBase ActSettings_activated() -1 -1 20 20 ActShowTree toggled(bool) MainBase ActShowTree_toggled(bool) -1 -1 20 20 ActBranch triggered() MainBase ActBranch_activated() -1 -1 20 20 ActTag triggered() MainBase ActTag_activated() -1 -1 20 20 ActDelete triggered() MainBase ActDelete_activated() -1 -1 20 20 ActViewFile triggered() MainBase ActViewFile_activated() -1 -1 20 20 ActViewDiff triggered() MainBase ActViewDiff_activated() -1 -1 20 20 ActViewRev triggered() MainBase ActViewRev_activated() -1 -1 20 20 ActSplitView triggered() MainBase ActSplitView_activated() -1 -1 20 20 ActFind triggered() MainBase ActFind_activated() -1 -1 20 20 ActFindNext triggered() MainBase ActFindNext_activated() -1 -1 20 20 ActBack triggered() MainBase ActBack_activated() -1 -1 20 20 ActForward triggered() MainBase ActForward_activated() -1 -1 20 20 ActCustomActionSetup triggered() MainBase ActCustomActionSetup_activated() -1 -1 20 20 ActViewFileNewTab triggered() MainBase ActViewFileNewTab_activated() -1 -1 20 20 ActViewDiffNewTab triggered() MainBase ActViewDiffNewTab_activated() -1 -1 20 20 tabWdg currentChanged(int) MainBase tabWdg_currentChanged(int) 20 20 20 20 ActSearchAndFilter toggled(bool) MainBase ActSearchAndFilter_toggled(bool) -1 -1 487 238 ActSearchAndHighlight toggled(bool) MainBase ActSearchAndHighlight_toggled(bool) -1 -1 487 238 ActShowDescHeader triggered() MainBase ActShowDescHeader_activated() -1 -1 487 238 ActToggleLogsDiff triggered() MainBase ActToggleLogsDiff_activated() -1 -1 452 238 ActRangeDlg triggered() MainBase ActRangeDlg_activated() -1 -1 452 238 ActAmend triggered() MainBase ActAmend_activated() -1 -1 452 238 ActMarkDiffToSha triggered() MainBase ActMarkDiffToSha_activated() -1 -1 452 238
qgit-2.7/src/myprocess.cpp000066400000000000000000000145311305655150700156600ustar00rootroot00000000000000/* Description: interface to sync and async external program execution Author: Marco Costalba (C) 2005-2007 Copyright: See COPYING file that comes with this distribution */ #include #include #include "exceptionmanager.h" #include "common.h" #include "domain.h" #include "myprocess.h" MyProcess::MyProcess(QObject *go, Git* g, const QString& wd, bool err) : QProcess(g) { guiObject = go; git = g; workDir = wd; runOutput = NULL; receiver = NULL; errorReportingEnabled = err; canceling = async = isWinShell = isErrorExit = false; } bool MyProcess::runAsync(SCRef rc, QObject* rcv, SCRef buf) { async = true; runCmd = rc; receiver = rcv; setupSignals(); if (!launchMe(runCmd, buf)) return false; // caller will delete us return true; } bool MyProcess::runSync(SCRef rc, QByteArray* ro, QObject* rcv, SCRef buf) { async = false; runCmd = rc; runOutput = ro; receiver = rcv; if (runOutput) runOutput->clear(); setupSignals(); if (!launchMe(runCmd, buf)) return false; QTime t; t.start(); busy = true; // we have to wait here until we exit while (busy) { waitForFinished(20); // suspend 20ms to let OS reschedule if (t.elapsed() > 200) { EM_PROCESS_EVENTS; t.restart(); } } return !isErrorExit; } void MyProcess::setupSignals() { connect(git, SIGNAL(cancelAllProcesses()), this, SLOT(on_cancel())); connect(this, SIGNAL(readyReadStandardOutput()), this, SLOT(on_readyReadStandardOutput())); connect(this, SIGNAL(finished(int, QProcess::ExitStatus)), this, SLOT(on_finished(int, QProcess::ExitStatus))); if (receiver) { connect(this, SIGNAL(readyReadStandardError ()), this, SLOT(on_readyReadStandardError())); connect(this, SIGNAL(procDataReady(const QByteArray&)), receiver, SLOT(procReadyRead(const QByteArray&))); connect(this, SIGNAL(eof()), receiver, SLOT(procFinished())); } Domain* d = git->curContext(); if (d) connect(d, SIGNAL(cancelDomainProcesses()), this, SLOT(on_cancel())); } void MyProcess::sendErrorMsg(bool notStarted, SCRef err) { if (!errorReportingEnabled) return; QString errorDesc(readAllStandardError()); errorDesc.prepend(err); if (notStarted) errorDesc = QString::fromLatin1("Unable to start the process!"); const QString cmd(arguments.join(" ")); // hide any QUOTE_CHAR or related stuff MainExecErrorEvent* e = new MainExecErrorEvent(cmd, errorDesc); QApplication::postEvent(guiObject, e); } bool MyProcess::launchMe(SCRef runCmd, SCRef buf) { arguments = splitArgList(runCmd); if (arguments.isEmpty()) return false; setWorkingDirectory(workDir); if (!QGit::startProcess(this, arguments, buf, &isWinShell)) { sendErrorMsg(true); return false; } return true; } void MyProcess::on_readyReadStandardOutput() { if (canceling) return; if (receiver) emit procDataReady(readAllStandardOutput()); else if (runOutput) runOutput->append(readAllStandardOutput()); } void MyProcess::on_readyReadStandardError() { if (canceling) return; if (receiver) emit procDataReady(readAllStandardError()); // redirect to stdout else dbs("ASSERT in myReadFromStderr: NULL receiver"); } void MyProcess::on_finished(int exitCode, QProcess::ExitStatus exitStatus) { // Checking exingStatus is not reliable under Windows where if the // process was terminated with TerminateProcess() from another // application its value is still NormalExit // // Checking exit code for a failing command is unreliable too, as // exmple 'git status' returns 1 also without errors. // // On Windows exit code seems reliable in case of a command wrapped // in Window shell interpreter. // // So to detect a failing command we check also if stderr is not empty. QString errorDesc(readAllStandardError()); isErrorExit = (exitStatus != QProcess::NormalExit) || (exitCode != 0 && isWinShell) || !errorDesc.isEmpty() || canceling; if (!canceling) { // no more noise after cancel if (receiver) emit eof(); if (isErrorExit) sendErrorMsg(false, errorDesc); } busy = false; if (async) deleteLater(); } void MyProcess::on_cancel() { canceling = true; #ifdef Q_OS_WIN32 kill(); // uses TerminateProcess #else terminate(); // uses SIGTERM signal #endif waitForFinished(); } const QStringList MyProcess::splitArgList(SCRef cmd) { // return argument list handling quotes and double quotes // substring, as example from: // cmd some_arg "some thing" v='some value' // to (comma separated fields) // sl = // early exit the common case if (!( cmd.contains(QGit::QUOTE_CHAR) || cmd.contains("\"") || cmd.contains("\'"))) return cmd.split(' ', QString::SkipEmptyParts); // we have some work to do... // first find a possible separator const QString sepList("#%&!?"); // separator candidates int i = 0; while (cmd.contains(sepList[i]) && i < sepList.length()) i++; if (i == sepList.length()) { dbs("ASSERT no unique separator found."); return QStringList(); } const QChar& sepChar(sepList[i]); // remove all spaces QString newCmd(cmd); newCmd.replace(QChar(' '), sepChar); // re-add spaces in quoted sections restoreSpaces(newCmd, sepChar); // QUOTE_CHAR is used internally to delimit arguments // with quoted text wholly inside as // arg1 = <[patch] cool patch on "cool feature"> // and should be removed before to feed QProcess newCmd.remove(QGit::QUOTE_CHAR); // QProcess::setArguments doesn't want quote // delimited arguments, so remove trailing quotes QStringList sl(newCmd.split(sepChar, QString::SkipEmptyParts)); QStringList::iterator it(sl.begin()); for ( ; it != sl.end(); ++it) { if (((*it).left(1) == "\"" && (*it).right(1) == "\"") || ((*it).left(1) == "\'" && (*it).right(1) == "\'")) *it = (*it).mid(1, (*it).length() - 2); } return sl; } void MyProcess::restoreSpaces(QString& newCmd, const QChar& sepChar) { // restore spaces inside quoted text, supports nested quote types QChar quoteChar; bool replace = false; for (int i = 0; i < newCmd.length(); i++) { const QChar& c = newCmd[i]; if ( !replace && (c == QGit::QUOTE_CHAR[0] || c == '\"' || c == '\'') && (newCmd.count(c) % 2 == 0)) { replace = true; quoteChar = c; continue; } if (replace && (c == quoteChar)) { replace = false; continue; } if (replace && c == sepChar) newCmd[i] = QChar(' '); } } qgit-2.7/src/myprocess.h000066400000000000000000000023421305655150700153220ustar00rootroot00000000000000/* Author: Marco Costalba (C) 2005-2007 Copyright: See COPYING file that comes with this distribution */ #ifndef MYPROCESS_H #define MYPROCESS_H #include #include "git.h" class Git; //custom process used to run shell commands in parallel class MyProcess : public QProcess { Q_OBJECT public: MyProcess(QObject *go, Git* g, const QString& wd, bool reportErrors); bool runSync(SCRef runCmd, QByteArray* runOutput, QObject* rcv, SCRef buf); bool runAsync(SCRef rc, QObject* rcv, SCRef buf); static const QStringList splitArgList(SCRef cmd); signals: void procDataReady(const QByteArray&); void eof(); public slots: void on_cancel(); private slots: void on_readyReadStandardOutput(); void on_readyReadStandardError(); void on_finished(int, QProcess::ExitStatus); private: void setupSignals(); bool launchMe(SCRef runCmd, SCRef buf); void sendErrorMsg(bool notStarted = false, SCRef errDesc = ""); static void restoreSpaces(QString& newCmd, const QChar& sepChar); QObject* guiObject; Git* git; QString runCmd; QByteArray* runOutput; QString workDir; QObject* receiver; QStringList arguments; bool errorReportingEnabled; bool canceling; bool busy; bool async; bool isWinShell; bool isErrorExit; }; #endif qgit-2.7/src/namespace_def.cpp000066400000000000000000000347601305655150700164140ustar00rootroot00000000000000/* Author: Marco Costalba (C) 2005-2007 Copyright: See COPYING file that comes with this distribution Definitions of complex namespace constants Complex constant objects are not folded in like integral types, so they are declared 'extern' in namespace to avoid duplicating them as file scope data in each file where QGit namespace is included. */ #include #include #include #include #include #include #include #include #include #include #include "common.h" #include "git.h" #include "annotate.h" #ifdef Q_OS_WIN32 // ********* platform dependent code ****** const QString QGit::SCRIPT_EXT = ".bat"; static void adjustPath(QStringList& args, bool* winShell) { /* To run an application/script under Windows you need to wrap the command line in the shell interpreter. You need this also to start native commands as 'dir'. An exception is if application is 'git' in that case we call with absolute path to be sure to find it. */ if (args.first() == "git" || args.first().startsWith("git-")) { if (!QGit::GIT_DIR.isEmpty()) // application built from sources args.first().prepend(QGit::GIT_DIR + '/'); if (winShell) *winShell = false; } else if (winShell) { args.prepend("/c"); args.prepend("cmd.exe"); *winShell = true; } } #elif defined(Q_OS_MACX) // MacOS X specific code #include // used by chmod() #include // used by chmod() const QString QGit::SCRIPT_EXT = ".sh"; static void adjustPath(QStringList& args, bool*) { /* Under MacOS X, git typically doesn't live in the PATH So use GIT_DIR from the settings if available Note: I (OC) think that this should be the default behaviour, but I don't want to break other platforms, so I introduced the MacOS X special case. Feel free to make this the default if you do feel the same. */ if (args.first() == "git" || args.first().startsWith("git-")) { if (!QGit::GIT_DIR.isEmpty()) // application built from sources args.first().prepend(QGit::GIT_DIR + '/'); } } #else #include // used by chmod() #include // used by chmod() const QString QGit::SCRIPT_EXT = ".sh"; static void adjustPath(QStringList&, bool*) {} #endif // ********* end of platform dependent code ****** // definition of an optimized sha hash function static inline uint hexVal(const uchar* ch) { return (*ch < 64 ? *ch - 48 : *ch - 87); } uint qHash(const ShaString& s) { // fast path, called 6-7 times per revision const uchar* ch = reinterpret_cast(s.latin1()); return (hexVal(ch ) << 24) + (hexVal(ch + 2) << 20) + (hexVal(ch + 4) << 16) + (hexVal(ch + 6) << 12) + (hexVal(ch + 8) << 8) + (hexVal(ch + 10) << 4) + hexVal(ch + 12); } /* Value returned by this function should be used only as function argument, * and not stored in a variable because 'ba' value is overwritten at each * call so the returned ShaString could became stale very quickly */ const ShaString QGit::toTempSha(const QString& sha) { static QByteArray ba; ba = sha.toLatin1(); return ShaString(sha.isEmpty() ? NULL : ba.constData()); } const ShaString QGit::toPersistentSha(const QString& sha, QVector& v) { v.append(sha.toLatin1()); return ShaString(v.last().constData()); } // minimum git version required const QString QGit::GIT_VERSION = "1.5.5"; // colors const QColor QGit::BROWN = QColor(150, 75, 0); const QColor QGit::ORANGE = QColor(255, 160, 50); const QColor QGit::DARK_ORANGE = QColor(216, 144, 0); const QColor QGit::LIGHT_ORANGE = QColor(255, 221, 170); const QColor QGit::LIGHT_BLUE = QColor(85, 255, 255); const QColor QGit::PURPLE = QColor(221, 221, 255); const QColor QGit::DARK_GREEN = QColor(0, 205, 0); // initialized at startup according to system wide settings QColor QGit::ODD_LINE_COL; QColor QGit::EVEN_LINE_COL; QString QGit::GIT_DIR; /* Default QFont c'tor calls static method QApplication::font() that could be still NOT initialized at this time, so set a dummy font family instead, it will be properly changed later, at startup */ QFont QGit::STD_FONT("Helvetica"); QFont QGit::TYPE_WRITER_FONT("Helvetica"); // patches drag and drop const QString QGit::PATCHES_DIR = "/.qgit_patches_copy"; const QString QGit::PATCHES_NAME = "qgit_import"; // git index parameters const QString QGit::ZERO_SHA = "0000000000000000000000000000000000000000"; const QString QGit::CUSTOM_SHA = "*** CUSTOM * CUSTOM * CUSTOM * CUSTOM **"; const QString QGit::ALL_MERGE_FILES = "ALL_MERGE_FILES"; const QByteArray QGit::ZERO_SHA_BA(QGit::ZERO_SHA.toLatin1()); const ShaString QGit::ZERO_SHA_RAW(QGit::ZERO_SHA_BA.constData()); // settings keys const QString QGit::ORG_KEY = "qgit"; const QString QGit::APP_KEY = "qgit4"; const QString QGit::GIT_DIR_KEY = "msysgit_exec_dir"; const QString QGit::EXT_DIFF_KEY = "external_diff_viewer"; const QString QGit::EXT_EDITOR_KEY = "external_editor"; const QString QGit::REC_REP_KEY = "recent_open_repos"; const QString QGit::STD_FNT_KEY = "standard_font"; const QString QGit::TYPWRT_FNT_KEY = "typewriter_font"; const QString QGit::FLAGS_KEY = "flags"; const QString QGit::PATCH_DIR_KEY = "Patch/last_dir"; const QString QGit::FMT_P_OPT_KEY = "Patch/args"; const QString QGit::AM_P_OPT_KEY = "Patch/args_2"; const QString QGit::EX_KEY = "Working_dir/exclude_file_path"; const QString QGit::EX_PER_DIR_KEY = "Working_dir/exclude_per_directory_file_name"; const QString QGit::CON_GEOM_KEY = "Console/geometry"; const QString QGit::CMT_GEOM_KEY = "Commit/geometry"; const QString QGit::MAIN_GEOM_KEY = "Top_window/geometry"; const QString QGit::REV_GEOM_KEY = "Rev_List_view/geometry"; const QString QGit::CMT_TEMPL_KEY = "Commit/template_file_path"; const QString QGit::CMT_ARGS_KEY = "Commit/args"; const QString QGit::RANGE_FROM_KEY = "RangeSelect/from"; const QString QGit::RANGE_TO_KEY = "RangeSelect/to"; const QString QGit::RANGE_OPT_KEY = "RangeSelect/options"; const QString QGit::ACT_GEOM_KEY = "Custom_actions/geometry"; const QString QGit::ACT_LIST_KEY = "Custom_actions/list"; const QString QGit::ACT_GROUP_KEY = "Custom_action_list/"; const QString QGit::ACT_TEXT_KEY = "/commands"; const QString QGit::ACT_FLAGS_KEY = "/flags"; // settings default values const QString QGit::CMT_TEMPL_DEF = ".git/commit-template"; const QString QGit::EX_DEF = ".git/info/exclude"; const QString QGit::EX_PER_DIR_DEF = ".gitignore"; const QString QGit::EXT_DIFF_DEF = "kompare"; const QString QGit::EXT_EDITOR_DEF = "emacs"; // cache file const QString QGit::BAK_EXT = ".bak"; const QString QGit::C_DAT_FILE = "/qgit_cache.dat"; // misc const QString QGit::QUOTE_CHAR = "$"; using namespace QGit; // settings helpers uint QGit::flags(SCRef flagsVariable) { QSettings settings; return settings.value(flagsVariable, FLAGS_DEF).toUInt(); } bool QGit::testFlag(uint f, SCRef flagsVariable) { return (flags(flagsVariable) & f); } void QGit::setFlag(uint f, bool b, SCRef flagsVariable) { QSettings settings; uint flags = settings.value(flagsVariable, FLAGS_DEF).toUInt(); flags = b ? flags | f : flags & ~f; settings.setValue(flagsVariable, flags); } // tree view icons helpers static QHash mimePixMap; void QGit::initMimePix() { if (!mimePixMap.empty()) // only once return; QPixmap* pm = new QPixmap(QString::fromUtf8(":/icons/resources/folder.png")); mimePixMap.insert("#folder_closed", pm); pm = new QPixmap(QString::fromUtf8(":/icons/resources/folder_open.png")); mimePixMap.insert("#folder_open", pm); pm = new QPixmap(QString::fromUtf8(":/icons/resources/misc.png")); mimePixMap.insert("#default", pm); pm = new QPixmap(QString::fromUtf8(":/icons/resources/source_c.png")); mimePixMap.insert("c", pm); pm = new QPixmap(QString::fromUtf8(":/icons/resources/source_cpp.png")); mimePixMap.insert("cpp", pm); pm = new QPixmap(QString::fromUtf8(":/icons/resources/source_h.png")); mimePixMap.insert("h", pm); pm = new QPixmap(*pm); mimePixMap.insert("hpp", pm); pm = new QPixmap(QString::fromUtf8(":/icons/resources/txt.png")); mimePixMap.insert("txt", pm); pm = new QPixmap(QString::fromUtf8(":/icons/resources/shellscript.png")); mimePixMap.insert("sh", pm); pm = new QPixmap(QString::fromUtf8(":/icons/resources/source_pl.png")); mimePixMap.insert("perl", pm); pm = new QPixmap(*pm); mimePixMap.insert("pl", pm); pm = new QPixmap(QString::fromUtf8(":/icons/resources/source_py.png")); mimePixMap.insert("py", pm); pm = new QPixmap(QString::fromUtf8(":/icons/resources/source_java.png")); mimePixMap.insert("java", pm); pm = new QPixmap(*pm); mimePixMap.insert("jar", pm); pm = new QPixmap(QString::fromUtf8(":/icons/resources/tar.png")); mimePixMap.insert("tar", pm); pm = new QPixmap(*pm); mimePixMap.insert("gz", pm); pm = new QPixmap(*pm); mimePixMap.insert("tgz", pm); pm = new QPixmap(*pm); mimePixMap.insert("zip", pm); pm = new QPixmap(*pm); mimePixMap.insert("bz", pm); pm = new QPixmap(*pm); mimePixMap.insert("bz2", pm); pm = new QPixmap(QString::fromUtf8(":/icons/resources/html.png")); mimePixMap.insert("html", pm); pm = new QPixmap(*pm); mimePixMap.insert("xml", pm); pm = new QPixmap(QString::fromUtf8(":/icons/resources/image.png")); mimePixMap.insert("bmp", pm); pm = new QPixmap(*pm); mimePixMap.insert("gif", pm); pm = new QPixmap(*pm); mimePixMap.insert("jpg", pm); pm = new QPixmap(*pm); mimePixMap.insert("jpeg", pm); pm = new QPixmap(*pm); mimePixMap.insert("png", pm); pm = new QPixmap(*pm); mimePixMap.insert("pbm", pm); pm = new QPixmap(*pm); mimePixMap.insert("pgm", pm); pm = new QPixmap(*pm); mimePixMap.insert("ppm", pm); pm = new QPixmap(*pm); mimePixMap.insert("svg", pm); pm = new QPixmap(*pm); mimePixMap.insert("tiff", pm); pm = new QPixmap(*pm); mimePixMap.insert("xbm", pm); pm = new QPixmap(*pm); mimePixMap.insert("xpm", pm); } void QGit::freeMimePix() { qDeleteAll(mimePixMap); } const QPixmap* QGit::mimePix(SCRef fileName) { SCRef ext = fileName.section('.', -1, -1).toLower(); if (mimePixMap.contains(ext)) return mimePixMap.value(ext); return mimePixMap.value("#default"); } // geometry settings helers void QGit::saveGeometrySetting(SCRef name, QWidget* w, splitVect* svPtr) { QSettings settings; if (w && w->isVisible()) settings.setValue(name + "_window", w->saveGeometry()); if (!svPtr) return; int cnt = 0; FOREACH (splitVect, it, *svPtr) { cnt++; if ((*it)->sizes().contains(0)) continue; QString nm(name + "_splitter_" + QString::number(cnt)); settings.setValue(nm, (*it)->saveState()); } } void QGit::restoreGeometrySetting(SCRef name, QWidget* w, splitVect* svPtr) { QSettings settings; QString nm; if (w) { nm = name + "_window"; QVariant v = settings.value(nm); if (v.isValid()) w->restoreGeometry(v.toByteArray()); } if (!svPtr) return; int cnt = 0; FOREACH (splitVect, it, *svPtr) { cnt++; nm = name + "_splitter_" + QString::number(cnt); QVariant v = settings.value(nm); if (!v.isValid()) continue; (*it)->restoreState(v.toByteArray()); } } // misc helpers bool QGit::stripPartialParaghraps(const QByteArray& ba, QString* dst, QString* prev) { QTextCodec* tc = QTextCodec::codecForLocale(); if (ba.endsWith('\n')) { // optimize common case *dst = tc->toUnicode(ba); // handle rare case of a '\0' inside content while (dst->size() < ba.size() && ba.at(dst->size()) == '\0') { QString s = tc->toUnicode(ba.mid(dst->size() + 1)); // sizes should match dst->append(" ").append(s); } dst->truncate(dst->size() - 1); // strip trailing '\n' if (!prev->isEmpty()) { dst->prepend(*prev); prev->clear(); } return true; } QString src = tc->toUnicode(ba); // handle rare case of a '\0' inside content while (src.size() < ba.size() && ba.at(src.size()) == '\0') { QString s = tc->toUnicode(ba.mid(src.size() + 1)); src.append(" ").append(s); } int idx = src.lastIndexOf('\n'); if (idx == -1) { prev->append(src); dst->clear(); return false; } *dst = src.left(idx).prepend(*prev); // strip trailing '\n' *prev = src.mid(idx + 1); // src[idx] is '\n', skip it return true; } bool QGit::writeToFile(SCRef fileName, SCRef data, bool setExecutable) { QFile file(fileName); if (!file.open(QIODevice::WriteOnly)) { dbp("ERROR: unable to write file %1", fileName); return false; } QString data2(data); QTextStream stream(&file); #ifdef Q_OS_WIN32 data2.replace("\r\n", "\n"); // change windows CRLF to linux data2.replace("\n", "\r\n"); // then change all linux CRLF to windows #endif stream << data2; file.close(); #ifndef Q_OS_WIN32 if (setExecutable) chmod(fileName.toLatin1().constData(), 0755); #endif return true; } bool QGit::writeToFile(SCRef fileName, const QByteArray& data, bool setExecutable) { QFile file(fileName); if (!file.open(QIODevice::WriteOnly)) { dbp("ERROR: unable to write file %1", fileName); return false; } QDataStream stream(&file); stream.writeRawData(data.constData(), data.size()); file.close(); #ifndef Q_OS_WIN32 if (setExecutable) chmod(fileName.toLatin1().constData(), 0755); #endif return true; } bool QGit::readFromFile(SCRef fileName, QString& data) { data = ""; QFile file(fileName); if (!file.open(QIODevice::ReadOnly)) { dbp("ERROR: unable to read file %1", fileName); return false; } QTextStream stream(&file); data = stream.readAll(); file.close(); return true; } bool QGit::startProcess(QProcess* proc, SCList args, SCRef buf, bool* winShell) { if (!proc || args.isEmpty()) return false; QStringList arguments(args); adjustPath(arguments, winShell); QString prog(arguments.first()); arguments.removeFirst(); if (!buf.isEmpty()) { /* On Windows buffer size of QProcess's standard input pipe is quite limited and a crash can occur in case a big chunk of data is written to process stdin. As a workaround we use a temporary file to store data. Process stdin will be redirected to this file */ QTemporaryFile* bufFile = new QTemporaryFile(proc); bufFile->open(); QTextStream stream(bufFile); stream << buf; proc->setStandardInputFile(bufFile->fileName()); bufFile->close(); } QStringList env = QProcess::systemEnvironment(); env << "GIT_TRACE=0"; // avoid choking on debug traces env << "GIT_FLUSH=0"; // skip the fflush() in 'git log' proc->setEnvironment(env); proc->start(prog, arguments); // TODO test QIODevice::Unbuffered return proc->waitForStarted(); } qgit-2.7/src/patchcontent.cpp000066400000000000000000000231611305655150700163250ustar00rootroot00000000000000/* Author: Marco Costalba (C) 2005-2007 Copyright: See COPYING file that comes with this distribution */ #include #include #include "common.h" #include "domain.h" #include "git.h" #include "myprocess.h" #include "patchcontent.h" void DiffHighlighter::highlightBlock(const QString& text) { // state is used to count paragraphs, starting from 0 setCurrentBlockState(previousBlockState() + 1); if (text.isEmpty()) return; const bool useDark = QPalette().color(QPalette::Window).value() > QPalette().color(QPalette::WindowText).value(); QBrush blue = useDark ? Qt::darkBlue : QColor(Qt::cyan); QBrush green = useDark ? Qt::darkGreen : QColor(Qt::green); QBrush magenta = useDark ? Qt::darkMagenta : QColor(Qt::magenta); QBrush backgroundPurple = useDark ? QGit::PURPLE : QGit::PURPLE.darker(600); QTextCharFormat myFormat; const char firstChar = text.at(0).toLatin1(); switch (firstChar) { case '@': myFormat.setForeground(magenta); break; case '+': myFormat.setForeground(green); break; case '-': myFormat.setForeground(Qt::red); break; case 'c': case 'd': case 'i': case 'n': case 'o': case 'r': case 's': if (text.startsWith("diff --git a/")) { myFormat.setForeground(blue); myFormat.setBackground(backgroundPurple); } else if (text.startsWith("copy ") || text.startsWith("index ") || text.startsWith("new ") || text.startsWith("old ") || text.startsWith("rename ") || text.startsWith("similarity ")) myFormat.setForeground(blue); else if (cl > 0 && text.startsWith("diff --combined")) { myFormat.setForeground(blue); myFormat.setBackground(backgroundPurple); } break; case ' ': if (cl > 0) { if (text.left(cl).contains('+')) myFormat.setForeground(green); else if (text.left(cl).contains('-')) myFormat.setForeground(Qt::red); } break; } if (myFormat.isValid()) setFormat(0, text.length(), myFormat); PatchContent* pc = static_cast(parent()); if (pc->matches.count() > 0) { int indexFrom, indexTo; if (pc->getMatch(currentBlockState(), &indexFrom, &indexTo)) { QTextEdit* te = dynamic_cast(parent()); QTextCharFormat fmt; fmt.setFont(te->currentFont()); fmt.setFontWeight(QFont::Bold); fmt.setForeground(Qt::blue); if (indexTo == 0) indexTo = text.length(); setFormat(indexFrom, indexTo - indexFrom, fmt); } } } PatchContent::PatchContent(QWidget* parent) : QTextEdit(parent) { diffLoaded = seekTarget = false; curFilter = prevFilter = VIEW_ALL; pickAxeRE.setMinimal(true); pickAxeRE.setCaseSensitivity(Qt::CaseInsensitive); setFont(QGit::TYPE_WRITER_FONT); diffHighlighter = new DiffHighlighter(this); } void PatchContent::setup(Domain*, Git* g) { git = g; } void PatchContent::clear() { git->cancelProcess(proc); QTextEdit::clear(); patchRowData.clear(); halfLine = ""; matches.clear(); diffLoaded = false; seekTarget = !target.isEmpty(); } void PatchContent::refresh() { int topPara = topToLineNum(); setUpdatesEnabled(false); QByteArray tmp(patchRowData); clear(); patchRowData = tmp; processData(patchRowData, &topPara); scrollLineToTop(topPara); setUpdatesEnabled(true); } void PatchContent::scrollCursorToTop() { QRect r = cursorRect(); QScrollBar* vsb = verticalScrollBar(); vsb->setValue(vsb->value() + r.top()); } void PatchContent::scrollLineToTop(int lineNum) { QTextCursor tc = textCursor(); tc.movePosition(QTextCursor::Start); tc.movePosition(QTextCursor::NextBlock, QTextCursor::MoveAnchor, lineNum); setTextCursor(tc); scrollCursorToTop(); } int PatchContent::positionToLineNum(int pos) { QTextCursor tc = textCursor(); tc.setPosition(pos); return tc.blockNumber(); } int PatchContent::topToLineNum() { return cursorForPosition(QPoint(1, 1)).blockNumber(); } bool PatchContent::centerTarget(SCRef target) { moveCursor(QTextCursor::Start); // find() updates cursor position if (!find(target, QTextDocument::FindCaseSensitively | QTextDocument::FindWholeWords)) return false; // move to the beginning of the line moveCursor(QTextCursor::StartOfLine); // grap copy of current cursor state QTextCursor tc = textCursor(); // move the target line to the top moveCursor(QTextCursor::End); setTextCursor(tc); return true; } void PatchContent::centerOnFileHeader(StateInfo& st) { if (st.fileName().isEmpty()) return; target = st.fileName(); bool combined = (st.isMerge() && !st.allMergeFiles()); git->formatPatchFileHeader(&target, st.sha(), st.diffToSha(), combined, st.allMergeFiles()); seekTarget = !target.isEmpty(); if (seekTarget) seekTarget = !centerTarget(target); } void PatchContent::centerMatch(int id) { if (matches.count() <= id) return; //FIXME // patchTab->textEditDiff->setSelection(matches[id].paraFrom, matches[id].indexFrom, // matches[id].paraTo, matches[id].indexTo); } void PatchContent::procReadyRead(const QByteArray& data) { patchRowData.append(data); if (document()->isEmpty() && isVisible()) processData(data); } void PatchContent::typeWriterFontChanged() { setFont(QGit::TYPE_WRITER_FONT); setPlainText(toPlainText()); } void PatchContent::processData(const QByteArray& fileChunk, int* prevLineNum) { QString newLines; if (!QGit::stripPartialParaghraps(fileChunk, &newLines, &halfLine)) return; if (!prevLineNum && curFilter == VIEW_ALL) goto skip_filter; // optimize common case { // scoped code because of goto QString filteredLines; int notNegCnt = 0, notPosCnt = 0; QVector toAdded(1), toRemoved(1); // lines count from 1 // prevLineNum will be set to the number of corresponding // line in full patch. Number is negative just for algorithm // reasons, prevLineNum counts lines from 1 if (prevLineNum && prevFilter == VIEW_ALL) *prevLineNum = -(*prevLineNum); // set once const QStringList sl(newLines.split('\n', QString::KeepEmptyParts)); FOREACH_SL (it, sl) { // do not remove diff header because of centerTarget bool n = (*it).startsWith('-') && !(*it).startsWith("---"); bool p = (*it).startsWith('+') && !(*it).startsWith("+++"); if (!p) notPosCnt++; if (!n) notNegCnt++; toAdded.append(notNegCnt); toRemoved.append(notPosCnt); int curLineNum = toAdded.count() - 1; bool toRemove = (n && curFilter == VIEW_ADDED) || (p && curFilter == VIEW_REMOVED); if (!toRemove) filteredLines.append(*it).append('\n'); if (prevLineNum && *prevLineNum == notNegCnt && prevFilter == VIEW_ADDED) *prevLineNum = -curLineNum; // set once if (prevLineNum && *prevLineNum == notPosCnt && prevFilter == VIEW_REMOVED) *prevLineNum = -curLineNum; // set once } if (prevLineNum && *prevLineNum <= 0) { if (curFilter == VIEW_ALL) *prevLineNum = -(*prevLineNum); else if (curFilter == VIEW_ADDED) *prevLineNum = toAdded.at(-(*prevLineNum)); else if (curFilter == VIEW_REMOVED) *prevLineNum = toRemoved.at(-(*prevLineNum)); if (*prevLineNum < 0) *prevLineNum = 0; } newLines = filteredLines; } // end of scoped code skip_filter: setUpdatesEnabled(false); if (prevLineNum || document()->isEmpty()) { // use the faster setPlainText() setPlainText(newLines); moveCursor(QTextCursor::Start); } else { int topLine = cursorForPosition(QPoint(1, 1)).blockNumber(); append(newLines); if (topLine > 0) scrollLineToTop(topLine); } QScrollBar* vsb = verticalScrollBar(); vsb->setValue(vsb->value() + cursorRect().top()); setUpdatesEnabled(true); } void PatchContent::procFinished() { if (!patchRowData.endsWith("\n")) patchRowData.append('\n'); // flush pending half lines refresh(); // show patchRowData content if (seekTarget) seekTarget = !centerTarget(target); diffLoaded = true; if (computeMatches()) { diffHighlighter->rehighlight(); // slow on big data centerMatch(); } } int PatchContent::doSearch(SCRef txt, int pos) { if (isRegExp) return txt.indexOf(pickAxeRE, pos); return txt.indexOf(pickAxeRE.pattern(), pos, Qt::CaseInsensitive); } bool PatchContent::computeMatches() { matches.clear(); if (pickAxeRE.isEmpty()) return false; SCRef txt = toPlainText(); int pos, lastPos = 0, lastPara = 0; // must be at the end to catch patterns across more the one chunk while ((pos = doSearch(txt, lastPos)) != -1) { matches.append(MatchSelection()); MatchSelection& s = matches.last(); s.paraFrom = txt.mid(lastPos, pos - lastPos).count('\n'); s.paraFrom += lastPara; s.indexFrom = pos - txt.lastIndexOf('\n', pos) - 1; // index starts from 0 lastPos = pos; pos += (isRegExp ? pickAxeRE.matchedLength() : pickAxeRE.pattern().length()); pos--; s.paraTo = s.paraFrom + txt.mid(lastPos, pos - lastPos).count('\n'); s.indexTo = pos - txt.lastIndexOf('\n', pos) - 1; s.indexTo++; // in QTextEdit::setSelection() indexTo is not included lastPos = pos; lastPara = s.paraTo; } return !matches.isEmpty(); } bool PatchContent::getMatch(int para, int* indexFrom, int* indexTo) { for (int i = 0; i < matches.count(); i++) if (matches[i].paraFrom <= para && matches[i].paraTo >= para) { *indexFrom = (para == matches[i].paraFrom ? matches[i].indexFrom : 0); *indexTo = (para == matches[i].paraTo ? matches[i].indexTo : 0); return true; } return false; } void PatchContent::on_highlightPatch(const QString& exp, bool re) { pickAxeRE.setPattern(exp); isRegExp = re; if (diffLoaded) procFinished(); } void PatchContent::update(StateInfo& st) { bool combined = (st.isMerge() && !st.allMergeFiles()); if (combined) { const Rev* r = git->revLookup(st.sha()); if (r) diffHighlighter->setCombinedLength(r->parentsCount()); } else diffHighlighter->setCombinedLength(0); clear(); proc = git->getDiff(st.sha(), this, st.diffToSha(), combined); // non blocking } qgit-2.7/src/patchcontent.h000066400000000000000000000035231305655150700157720ustar00rootroot00000000000000/* Author: Marco Costalba (C) 2005-2007 Copyright: See COPYING file that comes with this distribution */ #ifndef PATCHCONTENT_H #define PATCHCONTENT_H #include #include #include #include "common.h" class Domain; class Git; class MyProcess; class StateInfo; class DiffHighlighter : public QSyntaxHighlighter { public: DiffHighlighter(QTextEdit* p) : QSyntaxHighlighter(p), cl(0) {} void setCombinedLength(uint c) { cl = c; } virtual void highlightBlock(const QString& text); private: uint cl; }; class PatchContent: public QTextEdit { Q_OBJECT public: PatchContent(QWidget* parent); void setup(Domain* parent, Git* git); void clear(); void centerOnFileHeader(StateInfo& st); void refresh(); void update(StateInfo& st); enum PatchFilter { VIEW_ALL, VIEW_ADDED, VIEW_REMOVED }; PatchFilter curFilter, prevFilter; public slots: void on_highlightPatch(const QString&, bool); void typeWriterFontChanged(); void procReadyRead(const QByteArray& data); void procFinished(); private: friend class DiffHighlighter; void scrollCursorToTop(); void scrollLineToTop(int lineNum); int positionToLineNum(int pos); int topToLineNum(); void saveRestoreSizes(bool startup = false); int doSearch(const QString& txt, int pos); bool computeMatches(); bool getMatch(int para, int* indexFrom, int* indexTo); void centerMatch(int id = 0); bool centerTarget(SCRef target); void processData(const QByteArray& data, int* prevLineNum = NULL); Git* git; DiffHighlighter* diffHighlighter; QPointer proc; bool diffLoaded; QByteArray patchRowData; QString halfLine; bool isRegExp; QRegExp pickAxeRE; QString target; bool seekTarget; struct MatchSelection { int paraFrom; int indexFrom; int paraTo; int indexTo; }; typedef QVector Matches; Matches matches; }; #endif qgit-2.7/src/patchview.cpp000066400000000000000000000113331305655150700156230ustar00rootroot00000000000000/* Description: patch viewer window Author: Marco Costalba (C) 2005-2007 Copyright: See COPYING file that comes with this distribution */ #include #include "common.h" #include "git.h" #include "mainimpl.h" #include "patchcontent.h" #include "patchview.h" PatchView::PatchView(MainImpl* mi, Git* g) : Domain(mi, g, false) { patchTab = new Ui_TabPatch(); patchTab->setupUi(container); SCRef ic(QString::fromUtf8(":/icons/resources/plusminus.png")); patchTab->buttonFilterPatch->setIcon(QIcon(ic)); QButtonGroup* bg = new QButtonGroup(this); bg->addButton(patchTab->radioButtonParent, DIFF_TO_PARENT); bg->addButton(patchTab->radioButtonHead, DIFF_TO_HEAD); bg->addButton(patchTab->radioButtonSha, DIFF_TO_SHA); connect(bg, SIGNAL(buttonClicked(int)), this, SLOT(button_clicked(int))); patchTab->textBrowserDesc->setup(this); patchTab->textEditDiff->setup(this, git); patchTab->fileList->setup(this, git); connect(m(), SIGNAL(typeWriterFontChanged()), patchTab->textEditDiff, SLOT(typeWriterFontChanged())); connect(m(), SIGNAL(changeFont(const QFont&)), patchTab->fileList, SLOT(on_changeFont(const QFont&))); connect(patchTab->lineEditDiff, SIGNAL(returnPressed()), this, SLOT(lineEditDiff_returnPressed())); connect(patchTab->fileList, SIGNAL(contextMenu(const QString&, int)), this, SLOT(on_contextMenu(const QString&, int))); connect(patchTab->buttonFilterPatch, SIGNAL(clicked()), this, SLOT(buttonFilterPatch_clicked())); } PatchView::~PatchView() { if (!parent()) return; clear(); // to cancel any data loading delete patchTab; } void PatchView::clear(bool complete) { if (complete) { st.clear(); patchTab->textBrowserDesc->clear(); patchTab->fileList->clear(); } patchTab->textEditDiff->clear(); } void PatchView::buttonFilterPatch_clicked() { QString ic; PatchContent* pc = patchTab->textEditDiff; pc->prevFilter = pc->curFilter; if (pc->curFilter == PatchContent::VIEW_ALL) { pc->curFilter = PatchContent::VIEW_ADDED; ic = QString::fromUtf8(":/icons/resources/plusonly.png"); } else if (pc->curFilter == PatchContent::VIEW_ADDED) { pc->curFilter = PatchContent::VIEW_REMOVED; ic = QString::fromUtf8(":/icons/resources/minusonly.png"); } else if (pc->curFilter == PatchContent::VIEW_REMOVED) { pc->curFilter = PatchContent::VIEW_ALL; ic = QString::fromUtf8(":/icons/resources/plusminus.png"); } patchTab->buttonFilterPatch->setIcon(QIcon(ic)); patchTab->textEditDiff->refresh(); } void PatchView::on_contextMenu(const QString& data, int type) { if (isLinked()) // skip if not linked to main view Domain::on_contextMenu(data, type); } void PatchView::lineEditDiff_returnPressed() { if (patchTab->lineEditDiff->text().isEmpty()) return; patchTab->radioButtonSha->setChecked(true); // could be called by code button_clicked(DIFF_TO_SHA); } void PatchView::button_clicked(int diffType) { QString sha; switch (diffType) { case DIFF_TO_PARENT: break; case DIFF_TO_HEAD: sha = "HEAD"; break; case DIFF_TO_SHA: sha = patchTab->lineEditDiff->text(); break; } if (sha == QGit::ZERO_SHA) return; // check for a ref name or an abbreviated form normalizedSha = (sha.length() != 40 && !sha.isEmpty() ? git->getRefSha(sha) : sha); if (normalizedSha != st.diffToSha()) { // avoid looping st.setDiffToSha(normalizedSha); // could be empty UPDATE(); } } void PatchView::on_updateRevDesc() { SCRef d = m()->getRevisionDesc(st.sha()); patchTab->textBrowserDesc->setHtml(d); } void PatchView::updatePatch() { PatchContent* pc = patchTab->textEditDiff; pc->clear(); if (normalizedSha != st.diffToSha()) { // note <(null)> != <(empty)> if (!st.diffToSha().isEmpty()) { patchTab->lineEditDiff->setText(st.diffToSha()); lineEditDiff_returnPressed(); } else if (!normalizedSha.isEmpty()) { normalizedSha = ""; // we cannot uncheck radioButtonSha directly // because "Parent" button will stay off patchTab->radioButtonParent->toggle(); } } pc->update(st); // non blocking } bool PatchView::doUpdate(bool force) { const RevFile* files = NULL; bool newFiles = false; if (st.isChanged(StateInfo::SHA) || force) { if (!isLinked()) { QString caption(git->getShortLog(st.sha())); if (caption.length() > 30) caption = caption.left(30 - 3).trimmed().append("..."); setTabCaption(caption); } on_updateRevDesc(); } if (st.isChanged(StateInfo::ANY & ~StateInfo::FILE_NAME) || force) { updatePatch(); patchTab->fileList->clear(); files = git->getFiles(st.sha(), st.diffToSha(), st.allMergeFiles()); newFiles = true; } // call always to allow a simple refresh patchTab->fileList->update(files, newFiles); if (st.isChanged() || force) patchTab->textEditDiff->centerOnFileHeader(st); return true; } qgit-2.7/src/patchview.h000066400000000000000000000016621305655150700152740ustar00rootroot00000000000000/* Author: Marco Costalba (C) 2005-2007 Copyright: See COPYING file that comes with this distribution */ #ifndef PATCHVIEW_H #define PATCHVIEW_H #include "ui_patchview.h" #include "domain.h" class Git; class PatchView :public Domain { Q_OBJECT public: PatchView() {} PatchView(MainImpl* mi, Git* g); ~PatchView(); void clear(bool complete = true); Ui_TabPatch* tab() { return patchTab; } signals: void diffTo(const QString&); void diffViewerDocked(); public slots: void on_updateRevDesc(); void lineEditDiff_returnPressed(); void button_clicked(int); void buttonFilterPatch_clicked(); protected slots: virtual void on_contextMenu(const QString&, int); protected: virtual bool doUpdate(bool force); private: void updatePatch(); void saveRestoreSizes(bool startup = false); Ui_TabPatch* patchTab; QString normalizedSha; enum ButtonId { DIFF_TO_PARENT = 0, DIFF_TO_HEAD = 1, DIFF_TO_SHA = 2 }; }; #endif qgit-2.7/src/patchview.ui000066400000000000000000000157401305655150700154640ustar00rootroot00000000000000 TabPatch 0 0 704 449 Patch 0 0 0 0 0 0 0 0 0 0 6 2 2 2 2 Diff to: false Qt::StrongFocus Check to diff against parent P&arent Alt+A true Qt::ClickFocus Check to diff against HEAD H&EAD Alt+E Qt::ClickFocus Use CTRL + right click to select a revision to diff against &SHA Alt+S Qt::Horizontal 20 0 1 0 SHA value of selected revision. Use CTRL + right click to select. Toggle view of added and removed lines (CTRL+H) Ctrl+H 0 0 Qt::Horizontal 5 0 Qt::Vertical QTextEdit::NoWrap false QTextEdit::NoWrap true Qt::TextSelectableByMouse 1 0 Qt::CustomContextMenu FileList QListWidget
filelist.h
RevDesc QTextBrowser
revdesc.h
PatchContent QTextEdit
patchcontent.h
revdesc.h
qgit-2.7/src/qgit.cpp000066400000000000000000000015761305655150700146050ustar00rootroot00000000000000/* Author: Marco Costalba (C) 2005-2007 Copyright: See COPYING file that comes with this distribution */ #include #include "common.h" #include "mainimpl.h" #if defined(_MSC_VER) && defined(NDEBUG) #pragma comment(linker,"/entry:mainCRTStartup") #pragma comment(linker,"/subsystem:windows") #endif using namespace QGit; int main(int argc, char* argv[]) { QApplication app(argc, argv); QCoreApplication::setOrganizationName(ORG_KEY); QCoreApplication::setApplicationName(APP_KEY); /* On Windows msysgit exec directory is set up * during installation so to always find git.exe * also if not in PATH */ QSettings set; GIT_DIR = set.value(GIT_DIR_KEY).toString(); initMimePix(); MainImpl* mainWin = new MainImpl; mainWin->show(); QObject::connect(&app, SIGNAL(lastWindowClosed()), &app, SLOT(quit())); bool ret = app.exec(); freeMimePix(); return ret; } qgit-2.7/src/rangeselect.ui000066400000000000000000000316711305655150700157670ustar00rootroot00000000000000 RangeSelectBase 0 0 415 235 Range select :/icons/resources/range_select.png true 0 4 4 4 4 6 0 0 0 0 0 0 0 0 2 2 0 0 Select a tag or paste/write a rev true QComboBox::NoInsert Top: false comboBoxTo &Bottom: false comboBoxFrom 0 0 Select a tag or paste/write a rev true QComboBox::NoInsert Qt::Vertical QSizePolicy::Expanding 395 16 QFrame::StyledPanel QFrame::Raised 6 9 9 9 9 6 0 0 0 0 Check to see git status. Necessary for committing. &Working dir Alt+W Check to enable '--all' option All branch&es Alt+E Check to view all the revisions Whole histor&y Alt+Y Qt::Vertical QSizePolicy::Expanding 395 16 6 0 0 0 0 0 0 &Additional options: false lineEditOptions 0 0 Extra options to feed 'git log' 6 0 0 0 0 0 0 You can always re-enable the dialog from settings. S&how this dialog when opening a repository Alt+H Qt::Horizontal QSizePolicy::Expanding 80 20 0 0 O&k Alt+K true pushButtonOk clicked() RangeSelectBase pushButtonOk_clicked() 20 20 20 20 checkBoxShowDialog toggled(bool) RangeSelectBase checkBoxShowDialog_toggled(bool) 20 20 20 20 checkBoxDiffCache toggled(bool) RangeSelectBase checkBoxDiffCache_toggled(bool) 78 98 207 117 checkBoxShowAll toggled(bool) RangeSelectBase checkBoxShowAll_toggled(bool) 207 98 207 117 checkBoxShowWholeHistory toggled(bool) RangeSelectBase checkBoxShowWholeHistory_toggled(bool) 336 98 207 117 qgit-2.7/src/rangeselectimpl.cpp000066400000000000000000000120441305655150700170070ustar00rootroot00000000000000/* Description: start-up dialog Author: Marco Costalba (C) 2005-2007 Copyright: See COPYING file that comes with this distribution */ #include #include #include "common.h" #include "git.h" #include "rangeselectimpl.h" using namespace QGit; RangeSelectImpl::RangeSelectImpl(QWidget* par, QString* r, bool repoChanged, Git* g) : QDialog(par), git(g), range(r) { setupUi(this); QStringList orl, tmp; orderRefs(git->getAllRefNames(Git::BRANCH, !Git::optOnlyLoaded), tmp); if (!tmp.isEmpty()) orl << tmp << ""; orderRefs(git->getAllRefNames(Git::RMT_BRANCH, !Git::optOnlyLoaded), tmp); if (!tmp.isEmpty()) orl << tmp << ""; orderRefs(git->getAllRefNames(Git::TAG, !Git::optOnlyLoaded), tmp); if (!tmp.isEmpty()) orl << tmp; // as default select first tag that is not also the current HEAD int defIdx = orl.count() - tmp.count(); if (!tmp.empty()) { SCRef tagSha(git->getRefSha(tmp.first(), Git::TAG, false)); if (!tagSha.isEmpty() && git->checkRef(tagSha, Git::CUR_BRANCH)) // in this case set as default tag the next one if any defIdx += (tmp.count() > 1 ? 1 : -1); } if (!orl.isEmpty() && orl.last().isEmpty()) orl.pop_back(); QString from, to, options; if (!repoChanged) { // range values are sensible only when reloading the same repo QSettings settings; from = settings.value(RANGE_FROM_KEY).toString(); to = settings.value(RANGE_TO_KEY).toString(); options = settings.value(RANGE_OPT_KEY).toString(); } comboBoxTo->insertItem(0, "HEAD"); comboBoxTo->insertItems(1, orl); int idx = repoChanged ? 0 : comboBoxTo->findText(to); if (idx != -1) comboBoxTo->setCurrentIndex(idx); else comboBoxTo->setEditText(to); comboBoxFrom->insertItems(0, orl); idx = repoChanged ? defIdx : comboBoxFrom->findText(from); if (idx != -1) comboBoxFrom->setCurrentIndex(idx); else comboBoxFrom->setEditText(from); comboBoxFrom->setFocus(); lineEditOptions->setText(options); int f = flags(FLAGS_KEY); checkBoxDiffCache->setChecked(f & DIFF_INDEX_F); checkBoxShowAll->setChecked(f & ALL_BRANCHES_F); checkBoxShowWholeHistory->setChecked(f & WHOLE_HISTORY_F); checkBoxShowDialog->setChecked(f & RANGE_SELECT_F); } void RangeSelectImpl::orderRefs(const QStringList& src, QStringList& dst) { // we use an heuristic to list release candidates before corresponding // releases as example v.2.6.18-rc4 before v.2.6.18 // match a (dotted) number + something else + a number + EOL QRegExp re("[\\d\\.]+([^\\d\\.]+\\d+$)"); // in ASCII the space ' ' (32) comes before '!' (33) and both // before the rest, we need this to correctly order a sequence like // // [v1.5, v1.5-rc1, v1.5.1] --> [v1.5.1, v1.5, v1.5-rc1] const QString rcMark(" $$%%"); // an impossible to find string starting with a space const QString noRcMark("!$$%%"); // an impossible to find string starting with a '!' typedef QMap OrderedMap; QRegExp verRE("([^\\d])(\\d{1,2})(?=[^\\d])"); OrderedMap map; FOREACH_SL (it, src) { QString tmpStr(*it); if (re.indexIn(tmpStr) != -1) tmpStr.insert(re.pos(1), rcMark); else tmpStr += noRcMark; // Normalize all numbers to 3 digits with leading zeros, so one-digit // version numbers are always smaller than two-digit version numbers // [v1.10.3, v1.5.1, v1.7.2] --> [v.1.10.3, v1.7.2, v1.5.1] // QMap automatically sorts by keys, so we only have to iterate over it // and return the original strings (stored as the data() in the map) while (tmpStr.contains(verRE)) tmpStr.replace(verRE, "\\10\\2"); map[tmpStr] = *it; } dst.clear(); FOREACH (OrderedMap, it, map) dst.prepend(it.value()); } void RangeSelectImpl::checkBoxDiffCache_toggled(bool b) { setFlag(DIFF_INDEX_F, b); } void RangeSelectImpl::checkBoxShowDialog_toggled(bool b) { setFlag(RANGE_SELECT_F, b); } void RangeSelectImpl::checkBoxShowAll_toggled(bool b) { QString opt(lineEditOptions->text()); opt.remove("--all"); if (b) opt.append(" --all"); lineEditOptions->setText(opt.trimmed()); setFlag(ALL_BRANCHES_F, b); } void RangeSelectImpl::checkBoxShowWholeHistory_toggled(bool b) { comboBoxFrom->setEnabled(!b); comboBoxTo->setEnabled(!b); setFlag(WHOLE_HISTORY_F, b); } void RangeSelectImpl::pushButtonOk_clicked() { if (testFlag(WHOLE_HISTORY_F)) *range = "HEAD"; else { *range = comboBoxFrom->currentText(); if (!range->isEmpty()) range->append(".."); range->append(comboBoxTo->currentText()); } // all stuff after "--" should go after range if (lineEditOptions->text().contains("--")) { QString tmp(lineEditOptions->text()); tmp.insert(tmp.indexOf("--"), *range + " "); *range = tmp; } else range->prepend(lineEditOptions->text() + " "); *range = range->trimmed(); // save settings before leaving QSettings settings; settings.setValue(RANGE_FROM_KEY, comboBoxFrom->currentText()); settings.setValue(RANGE_TO_KEY, comboBoxTo->currentText()); settings.setValue(RANGE_OPT_KEY, lineEditOptions->text()); done(QDialog::Accepted); } QString RangeSelectImpl::getDefaultArgs() { QSettings settings; return settings.value(RANGE_OPT_KEY).toString(); } qgit-2.7/src/rangeselectimpl.h000066400000000000000000000013061305655150700164530ustar00rootroot00000000000000/* Author: Marco Costalba (C) 2005-2007 Copyright: See COPYING file that comes with this distribution */ #ifndef RANGESELECTIMPL_H #define RANGESELECTIMPL_H #include "ui_rangeselect.h" class Git; class RangeSelectImpl: public QDialog, public Ui_RangeSelectBase { Q_OBJECT public: RangeSelectImpl(QWidget* par, QString* range, bool rc, Git* g); static QString getDefaultArgs(); public slots: void pushButtonOk_clicked(); void checkBoxDiffCache_toggled(bool b); void checkBoxShowAll_toggled(bool b); void checkBoxShowDialog_toggled(bool b); void checkBoxShowWholeHistory_toggled(bool b); private: void orderRefs(const QStringList& src, QStringList& dst); Git* git; QString* range; }; #endif qgit-2.7/src/resources/000077500000000000000000000000001305655150700151365ustar00rootroot00000000000000qgit-2.7/src/resources/1downarrow.png000066400000000000000000000004621305655150700177510ustar00rootroot00000000000000‰PNG  IHDRÉV%bKGD¾¾¾µ­@› pHYs  ÒÝ~ütIMEÕ !¿IDAT(Ïí’ÁmÃ0‡» âô•B×`÷ º.ïÞƒ~˜”åÄyød$–;w üë-¥Þäc®–±-ì@ê½±ŒÃHæ|>%€€œsU3ÊB2’F µ;[ ÃPv@;¼¿.›P´4?W§Y“_‡äkCìm(,$­É˲¤§™æy®+¦Œ#°´I4¥,éǺ¦iªL!Ñæ-¥¤—ÛÞês¿¯=±×wã¯f€q«ÃXÁåRÒúž7°‡Ü˜=^ IEND®B`‚qgit-2.7/src/resources/1uparrow.png000066400000000000000000000004611305655150700174250ustar00rootroot00000000000000‰PNG  IHDRÉV%bKGD¾¾¾µ­@› pHYs  ÒÝ~ütIMEÕ wÍÈ[¾IDAT(ÏíÝiÃ@„GÁm©¸€¸ÿtƒu—~â"T‘çۇ̓ÎJ бXvîcf¥?©î7£Ö$,@Ç÷ãâ¿—¥e©5ÍE¶…Ñn»Í‡àRKÒ ŒlË ·Í&ïÂ¥ ‰‘±Àò¶…­õë:o†S2¡°EÄ ¦Û "ÐùóÜÍɧ¡%Ñj^ëšokÑ÷}JÒJ’ðe ÑRæ1",;ñíÝÔ>ìé°¢™6й:73Žc§=¯/xÒ§!mgIEND®B`‚qgit-2.7/src/resources/bookmark.png000066400000000000000000000017441305655150700174570ustar00rootroot00000000000000‰PNG  IHDRÄ´l;gAMA¯È7ŠétEXtSoftwareAdobe ImageReadyqÉe<vIDATxÚb` ü(g˜ÿ¥”a>±êˆ‘EßÊþ3¼ÿó‡áï_AÁI é &"PÀ¬!ÌÀÄÿÿ1£ €ˆ2øß?†x6{G «ã‰Ñ@ þRÂÀÀÇ À |ƒQõý4(FAƒÿýgÈç´±²®ñ'W†¿ò é ¼*º”ÁÉVÈû ÆìŽÜ ÿÙîGåð€§Šÿ@Eþÿaøû›Áó °´ÿû‡AÝPN=ö-PÕW¨i†·S™>~ô¨îÁŸß Aêøøûáù†Ä4ô?20H]r”¨<;/ÐU| LÒú ò?X¦ :çgß;¿¾ÝºÈðûó'†ß?3|¾ó¨ÿû«ßA–0ãÿv†`Â-a`»tþqhà¾ù…¸¬À$Œe†¯û~0\ñœá×7†D€Å¿F†`Ìgñ``ÐøÀ@ùò33Ãû®¯f¥óD›Ã ˆÉw À0íçp`T Òp`NdøÀÁðþø†›áû—¡ÐæÃ@1¢¥Y áûyÙ˜ä~â7ôH'Ãûsßnocøðç?ƒ£õ† 0i€Â(+Þå2ü.hÅ4Á@{?_d`¸¾“áÿ¿ Ž–û†‚@a¤c¡Éàd³á÷PBƃ¿K¼—ÀøªE7ˆGn3`eE þÐàâ—#Øä»Á ˜ þÄ¿˜ˆ™ê¸ù!j±™@Añ4‰Á• j(ÈËO€IèÃãK Ù âŸ!ÁÁÍÉÀpÀ’ÁÝ€bÁæZV«@)î Ãû ¯ÞÁ† €iöýY†1!`F…–Ü@Ÿ½‡¸ú²9„áb`®´gºä×S†{@—>ÏpáÏ?G½- …FÛ 8>xÊpá 0ƒ~~ g #€©ÈÝ€ÂHn÷cî³ ¤ïÀɨQcÃlaxЊ¡˜zê¹YÞexàqAY €˜°Ô ßIXJ(â2ì1LøÿŸÁðã7† =èò8š?ÜøU•IEND®B`‚qgit-2.7/src/resources/bookmark_add.png000066400000000000000000000013041305655150700202570ustar00rootroot00000000000000‰PNG  IHDRísO/ pHYs  šœgAMA±Ž|ûQ“ cHRMz%€ƒùÿ€éu0ê`:˜o’_ÅF:IDATxÚb`À>U1Ì_Î0Ÿ€bÄ%ñ¡’A€‰Ÿýþïß ¿ÞüT”œÈð›:€bÂeÀÿÿ ¬ºòlzZÿ0àR@8 ``cŠgsÐcàrRføÇÊK@a5àmC³ª´#çcFîÇ ê OšDþýÛ-€® €ÿu1ÌÿûAá/Я9Ùþqó3€ØÜ– Ì"/ÁŠþ¼”cx>ãÃß¿ ß_|d``çøðñþ§ @¯= Æ¿ óÿ©ª'0;Y%ÞAÍ}ŠÃÁ"@ÌÎð÷é†{³N2¼<÷n@cá{CÁ?>æ~Ž}FæoXµMfGá_8q‘áþþÿŒÄâp60Lx_ñ÷ïe׿s[ñ1°ˆp`1‚a}û† <\4" @ð@ìÊp ~Øû˜¾cb­PÍn<€ @,(!*­*ÀúõÃÇŸ@§VÄÓŠŽŒÿ%=}dÏ J†¿üføuùà é ÷o\d¨oGMKþò` €P’òóVÑÿœo¿02<òáíÛÿþ£T@ˆÑ 䯄 ö`u /lìxD€‚k%’‡YLÿ_e2ü¿ÅðþR(C$Iÿg8êÁpÔá½~Ãÿüòÿ@ÂÿÁr7àv"ƒÂÍx°æ†+ ˆ<Q¸ÏA`· CHó3˜8@€1Ü!zýIEND®B`‚qgit-2.7/src/resources/cancel.png000066400000000000000000000015721305655150700170760ustar00rootroot00000000000000‰PNG  IHDRóÿabKGDÿÿÿ ½§“ pHYs  @¾áAtIMEÓ ",§á¢‚IDATxÚm“[huÅßfg/Îî&»ÙK.²hR)ÆmjšÒÔ‚¦-TôA€Š`¨ VÑWT)*QV+¾}Ð Ú ´”–^Ò&©-6YldÍRsÙ°—l6îdgƇh´M?øÞÎ9p¾s>á6sy[¿ÏqÝ>º\\q‘ë]¼p¶v+Vn!š¯y\÷ù RáÆ4|Ù,%Û)ׄ¯Jºç­}çÏü±Aàò¶þ¤‚“ÑD|søé§Ðv  …‚¸u‹ÚØ8ÕÏ¿`>;U˜õûŸxøÌ©“ë½ÛM%r±­wëfßËуæíœá¬6(~v„¹/Nšæ½C§Nü©hm;–L>êñô‰§¥+ŸÇ©ÕÖ¥0q´x í·é€ÌεGö¨‹½}><çÝ·»ZEFÑ‚AŒT k¾€5_ÀYµñoÚ„ âÚòÀ.BŽ3´e&ߦ}¦a„íæ&œBÒÇ ïy#‘àŽtµ_®Ü~?‹ß~‡ùɧ´W–p•2;+Õ:Ð¥"…¢i ‹G¿&òøcx;Ú1ö&X:üŸOà]ZŸ¦áéÒÑ, -7ƒbø 07o¾±v½ñ ‚G¿ù/7W‚#¢”‹\wÊe¢SY"W®Ò|iŒ@(„÷d¶¦áƒCÿèu§ÁŠH^Utm´lÛeA4…¤{ÐÞ}g |ðxéÕu÷Ã÷°~lŸ¢í,_‹4«Á gk5áȬ*8Ï>³æùÐûëuŠ÷tSü8ÃêT¹¯‡ÚžAf•°àÑåâ±0Ò?б£-;vt-ѵs¹ E2R)ª—ÆX8w.ÿS[ë“Ó‰Øèz•¿ØÝ™´¬‘p:}·ÙÝÈÍd×…êä$¥‰‰üédâÀ¯ÉøéL&S¹ öåî[Sµ•×ÀqWÊô65P/±~Ï-ë;ŸˆžNÄ®d2™Ê†o6¶ÌäÛ:+Õ‘.GD­ˆä¯EšÆsñØ `1“ÉXÿâÿö_&ûјIEND®B`‚qgit-2.7/src/resources/colorize.png000066400000000000000000000017771305655150700175060ustar00rootroot00000000000000‰PNG  IHDRóÿagAMA¯È7ŠétEXtSoftwareAdobe ImageReadyqÉe<‘IDATxÚbüÿÿ?% €7BL@s~ÿdàeædTR÷ø.l`*!+ÁËÉÍÉðé峿¹þàõ]+/þ]öêÃ}˜Ä2`“û2xœj4;õæüªÿ{.øêå¿ÿûÿûÿöÏ¿ÿÿþüüÿ÷Ńÿ߯nû¿·Ñù‘‡,C2L?@A¾ÿ±N³Rdã¯iþþþéÿþþøÿM±þÿ8½þÿ*©þÿÅÏÓÿþ¨vÿþÆœÿþ®ˆÿþòÜÿܦhþ>nA¾ÿZ';>>©Ôßðá®—ÖõÌ .) ø î[Ý Ë“óÎûÚÁ$6/7+ÚB·Çâ¿A¾ÿpS$œ"'f{µa»Òí÷ßÓ }[(3IíU × óû'ÿ  # æÌ»ˆEÔÐÈJÂ܆arÁõöŸïßåÈêiýÿ÷…íãe`¨þb`øûÄ ¬L@šõçy Ò'â7lÔy­ˆ…ùó f1V×¼p-†µ@Š€üë'à~ÿÄ¿€|á' À¨úÿÿ>ƒ/3@1ÿùœO›åµ?'Ð6 eŸß10|âï¾½â·@þ†/oÀôO ?²2°?xËpÿÁ§ŸA¾ÿÉäý½ÄÅäÉÕÊØýÿò( ýúøöú÷ùòÿòïøóÐùûÝ;~ôIûÚ±ÜÐòíˆϾ3Ü´áàé7WÕ˜ß:*+òñ€½ðã ÿÿ ßDüØþÿaXüïC§ÑG†Íü—>\¾ÆðiÿŸcÄ„œ,¯bØP°ìmêÕ“÷þÎðïË7†ë/r^Iu2Üú»˜Áü;ƒæ×û Ê_î0ÈßÿÁ°ö>ÀbAOÛ×ÌÙñbYŸcˆ·ùÁá÷ñ¶0ƒ'/Êu~þ`f¨ÛÅpîÒ'†µÄŒ-ƒ<þÀp÷çwwuá?¢²—Dd¾3(ýoeâºÂðåí?†){ÿÞê¿ÀÿóÃ%€bĕ˸™”¼²¥œE¸ĸÙ_|bø¾÷Ãî=æþøÇp¨ì/@1‘cE€˜”sÀ‰„á(€  À†ªV6ʤIEND®B`‚qgit-2.7/src/resources/configure.png000066400000000000000000000016271305655150700176330ustar00rootroot00000000000000‰PNG  IHDRÄ´l;gAMA±Ž|ûQ“NIDAT8Ë¥•ûKSaÇý¡* ¢Ñ…ÒÈ<;3bÐýÚŸ”T!] Ð¼uE‹&: Ú–©óäe¶6k¨lδ µTr;ó®[M6“Y˜'M+²üvΙ]VÖZ>ðáå=—/Ïû<ïðK˜LLggFG°Zmhkc`2=¥Æ'a+‰úú6ÚbÁÏ15õÍÍÝÐëÇ®H®Õ¶ÐÝÝ^òOŸÐÔÔæÁÞÉ«ªŒtG‡ÕK>33‡ÊJÃØ? nݺOß“,÷®´T¯çóü-ÑÒÒ î‰O1M7ð²ry×Çü\­6±³³óXXøü³y……š$ŸbµºN§wØììÛ‚œËËëX~Óæç?Ìͽèíµ!'§RêS¬RÕega³9!“U°RiI¬R©c'&^czzö7¸²CFF™ïêàen÷&'ß ¯ÏΧÇKa¾ …©© ß•‘’’O p¹&Áí…Ã1®®~𕉻"2Év#’ø£8>>',99µX†a·/ ß…=š¤½U}ÎZ å‘L¾ØKÎÏù‰™’„„ë¬N×"lÎà c‰==CàÛœÍUq°«Dpåp7î„%WÌôÈÄ^ù‘#W$11—[ÏœÉEf¦ \¹!=½iiJA:4䀹:#%$^É |) Ät‰†Ht^3O2þ"ç#*êRÇ^éÒ¸/>>—í(‹ÃÀMoTÁøR T{˜¹‚—z ­WÅÌÃ+áW{מß-}V Æ»ÚMX¬ß4x3Ï=wéH˜R)Æxɹá"E4œ§—†Ú"<´“†slÇçÛ0^ºs£OñC~/™"t‰3¦æ¤Ö]@ŸÄ#6†5!@E°z[I8´‰”Þ¯”ÔĉˆêÓ"æy'wìñHµ€‚Õ@ÜX Ôo†þlü>Né“"Bu\ÄŒªvxrlØ ÈV…k€"N|7† áø¯³º8VD(Š˜á¬€š«Žž€f=PNà£*eÇ#Øÿ¾äÑ$‘wˆdÌi\*€2‹ÖA{* y‡IéŠnYIdí'[ÑÛQ},yw°Ù<Ò¯‰~Óƒ˜„ÄŒIEND®B`‚qgit-2.7/src/resources/editcopy.png000066400000000000000000000014111305655150700174610ustar00rootroot00000000000000‰PNG  IHDRóÿagAMA¯È7ŠétEXtSoftwareAdobe ImageReadyqÉe<›IDATxÚb}úÆðýû/>>nKK=;QQYÀpà*á²ýß¿³^c ¾~e`xÿþ 0¾0)me +Å•?þ £¿@5?aÃ4° €P øñƒáÛ·àX¹‚!ÍÍÍŒ!N†×¯?0|þü6Ý?~ x€¼öë×o& $ Èß èÏ `VIIQ9..ŽB€‚ðëÈÿÌà0À@±ÀÎÎÊ ..Ì --4• €X@l 0@Š1"è½_À°ùNlœœì`WËçÏß~ú Ä?ÀÀÐðè¢?`0ðùó?`àýz‹œà0b@±ìÜy|£X÷ýûÏﺺªNJʰÀýóç7Ðÿ,`—¼q!#@‚9Y3Èf ažtŒ2HÝß¿ÿ€62 ù¦A^ @(ÑÊ0 61чÈ+þ0‚m‚$sF°7‘@Á Ù¢¬,Ë ¯¯N®ÿÿÿkdee‡Å¿¬@úÿ¦X&°8@± 'a` c8zô<4]üÿyóæó ”v„ý¬þË—ï Žöõ€ÛbúlIEND®B`‚qgit-2.7/src/resources/encrypted.png000066400000000000000000000013561305655150700176460ustar00rootroot00000000000000‰PNG  IHDRóÿabKGDÿÿÿ ½§“ pHYs  ÒÝ~ütIMEÓ $84Fe{IDATxÚ}’MHTQÇÎ÷èè8ø1©ÉI‚R*CPµw)DëZ¸j„p¶™n‚ (ÂE«\XÐÊÖ"4ð;”!Mg†qÞ8Ï÷î¼ïÓŒS’guî=ÿÿï\ιuüÓÓo#mmᇭ­Íššê¯ȲšÊf¥™\®øzròT«wצ¦^µ„áD<Þÿlh¨7‹µÓÕÕBww{s à»NçÝñøýÕÅÅyqH̸êëc##7¦z{»vw÷ååµÂÖVJUUÅÝÓsÙïóy®§RéÃãkKKO ëFC4Å¢áÍÍ=maáËUÕžÊÊJÝSI*<è G£ÍãùüÉ@pUBh¿ßwÅïw³¾¾—Éd¤—ÉäÄÏdrâXˆRr{ûà@×>ŸçŠZµq (*†¡#„ “ÉŸ˜¦¹Z©ÍÎ&²²¬hšVB×uE­Î­Jºyi3„èô i¯™õLÎuNöW…s¿$¯ušÆUÊxïv¼  `5Ù7ê Ö'¬ÈÕ‘Pc(p"D$ ¯×nèX„ú›šÃAõT)y {ˆP_ <ÙøìÈJÜ‹ÝmêÈÇ€„àP- †äÁqÅŽ;_ßï~Ê€£œ¥î@1Ç…á’ÀÚ8Ê…Îfp˜s0 *ø´ ÌKpp‚!9æœ3@¾è`5ðŠ¿ •®– ŠÇ*VÉE¾Xp@5À¯«lL»l4mÐ-(`Ø`X »«üê1ì2Iÿ#2l0­²Ù´Ï̦:çÿp‚g¢* V¹7m„pŸì̒寿Û)¿ÄÿåŠî’€ßݹ[è¯ÿ”IEND®B`‚qgit-2.7/src/resources/filter.png000066400000000000000000000014641305655150700171360ustar00rootroot00000000000000‰PNG  IHDRóÿa pHYs  šœgAMA±Ž|ûQ“ cHRMz%€ƒùÿ€éu0ê`:˜o’_ÅFªIDATxÚbøÿÿ?ƒŠŠ ƒ„„ÃöíÛ@ÀÞÞÞ4---ˆÓ3€ 999ÚÉÉÉ$×ÝÝÍÀÊÊ VÒ @`BFF†‡‡GdõêÕ»^½zõÿÁƒÿ:ôùòåÿgΜùÖ¬YÿW­ZõÿøñãÿÛÛÛwõòà  0ÁÇÇ'õ Ž;ö?))鿦¦æ ¡ÿAêqWW×ÿùóçÿ²™AzlÀܹs×½|ùòDDÄyyy°B‘ÿºººÿ---ÿ›ššþ—““ƒrëÖ­ÿ@o§€ôØ §lWRR°‘………_YYùÿÇ q7^€‚ø/^|dÈ‘#Gþï¿¡¡áiii°‹€ ÖxíÚ5°7gÀ €à€ÀÆýÇž?þäÍØØØ0õ ½„b°ïÛ·ïóß¿ÿŸ>}ú?ýýúõÿ§NúÿÀÿëêên"+é ¸ÌÌÌ ŒŒŒ ¦øùóçÿƒ¢sÆ  ÿöìÙÿK—.ý ”F7 €à…ƒƒŒ EíÒ¥KÿOœ8ñÿðáÃÿwïÞ R¤Ã€@zÅ C¸¹¹Aé‚ÁÑѱæŸÉ XH/@‰ÈÈH0g ej`°°°Èùþý;ØWW×þ¨¨(Pt3¤¦¦2c ŒAzL1‚q@@ƒ··7ƒ••UÑ‹/þÿùómS‚ƒƒÁ–€ ‚a^€aaa(d ‹‹KÙãÇÿûöí?ÐÀ™ÀTÊŒB Ò @`h“.ƒ™™ƒ––V4(ôïß¿ÿßÈȨßÎÎ. À\ Ç ½„žPÐB}}ý‰@&®@0()ª†~¬ºIEND®B`‚qgit-2.7/src/resources/find.png000066400000000000000000000017331305655150700165700ustar00rootroot00000000000000‰PNG  IHDRóÿabKGDÿÿÿ ½§“ pHYs  šœtIMEÔ 0¿J¨hIDATxÚmÑ[h[uðïþÿsrnI›“&mº®fu…ÙÌâì|qs«ÓÁyƒ ‚{™ :/xÑ)">:öâËD>È`¬‹®LƦt£¥n½ º,MNº¦'Yšœsr=´PdßóÇ>§OŸ qÛ/ŠÂgO§GȚ븗j¶}¢UÇÇ_má¿ùzlen!÷ó¾½OüBOž<ò9ÿ])(£v‚ANR$I%1éöհsϾ²˜Jõ@íÛm㺪ªÇXËs^TÕà‡‘¨¦iá­¶‹F£ J)é´Ú_ÙµÕÛÒï|w±ƒ#ܡҦ®ËK9ã(xï…µ­³3„®H'bZcÐÂ!ÈrÁ Bzzc;ì€vÛù!«»³Ù‰jÕìç@¼²"! 0ˆ‚F£Eª,AEÄzbpœæ(d– (eXU ¶iQFDJ)jµ–WKª"Cà|Ÿ#žÀ£Õl¨0÷ׯ_D»5«6îûþ¬ZµmBˆâûåžg€À󸝡^o®€>“Êê3©]ëp%£-Ëã–i-ï^qÿÙô}»¾Y`SÙ»ù‘;ùÀÖLkpîg z,¬ó-IEND®B`‚qgit-2.7/src/resources/folder.png000066400000000000000000000011221305655150700171130ustar00rootroot00000000000000‰PNG  IHDRóÿa pHYs  šœgAMA±Ž|ûQ“ cHRMz%€ƒùÿ€éu0ê`:˜o’_ÅFÈIDATxÚbd€£’Ûÿþ300ÙÿÁ$Xü\*#@`dXpíÿ×ßÿýùAC1HŸ^€b1Ƚô{«:Ãç_P¨å07èñÞf`Ƚøÿ?#HŒ‘ìL¨üùÉúŒÄÂð÷Ãçߘ&ÿÿTÄÄÀPSèá3@ø @q&?ìï™ÿÄòÿÏ/†?ð¸hUTÙ’¬ Äòè‚wPç3ñ_ýBgÖ]a¨L—eˆ3ãGѺá*Ce߀bùÿï/Ä@›jZ0mJŒ’kžph(,>‚ €€.øÍðá'CgÛ9Ï@#†‰A˜Í_‹\dð÷×o€†ÁO†÷@üýù‹áé;†à9[@Éá?f@ĀğŸ?ì…O@üýþƒáå{¨ x°#ÙŠœœ r =DPøä¼ÿŠC3š̬¿ß~08 ¾]ðûûW†« DƒO2¤îùÎ@L ÿþÀM#üýö €€.øÃðö;Ð?¾ƒM%üê °wÔ½§ÿtéjP#ûœÐBÈýþ8Ÿ €ÿÿÿÏ@ 0ÈÒP:¸’\IEND®B`‚qgit-2.7/src/resources/folder_open.png000066400000000000000000000012711305655150700201410ustar00rootroot00000000000000‰PNG  IHDRóÿa pHYs  šœgAMA±Ž|ûQ“ cHRMz%€ƒùÿ€éu0ê`:˜o’_ÅF/IDATxÚbd€£’Ûÿþ300ÙÿÁ$Xü\*#@`dXpíÿ×ßÿýùAC1HŸ^€b1Ƚô{«:Ãç_P¨å07èñÞf`Ƚøÿ?#HŒ‘ìL¨üùÉúŒÄÂð÷Ãçߘ&ÿÿTÄÄÀPSèá3@ø @q&?ìï™ÿÄòÿÏ/†?ð¸hUDÙÅ­L ÌÞ˜’¬ Äòè‚wPç3ñ_ýB'Ÿ¿–-ã]ì.ÂÀËôÈù@CÏ €€.øÍðô݆™Ó¯1„Eè±5»3€m…'(p3V10°³Àc ~þüÃðýó·7 ƒŸ _ü+¾ýŠ!h6ÂSÿc ˜øô÷ïoߨ |dø÷Ÿ…áù ß ``D²)91²]öò3Ãÿï_®8_=ýÀðó?+ÃÛÏ@õ?ÿG5 Æfbg`øöúв_wn¾ahJVbˆ÷a <¾y!¼þ&õï.ÿ? ?>~fP•ãa |úö‹áÍëï ÿ~~»@@üaøúð)Ë×Þ<`†Ç>ÀÍÉÌðáí7†ßŸ?= °×8e'þÇ j¤Pg´Ðr¿?Îg ÆÿÄX‰ÇùµÍ(ˆ|IEND®B`‚qgit-2.7/src/resources/help.png000066400000000000000000000022421305655150700165740ustar00rootroot00000000000000‰PNG  IHDRÄ´l;bKGDÿÿÿ ½§“ pHYs  ­#½utIMEÓ 0%§ Â/IDATxÚ••MlTUÇ÷¾ï™)S H)iR@D‰ DÒøÕ„¥¡‰»&î *1nº’`‰+bc¢ ÌÒT7&kS5A)!ø¡"R[;BiéL§3óæ½y÷¸xmÓò%Þä¿9÷œÿ=9÷ÎQüÇ9zZ|:€œÆ°œó—Ï«ðqqêQýg¤4†z€¼H iq1Œ]ùP•Ÿˆ¸ÿŒä€!¥ÎyRèÚŒSØfÔŽ|‚ÖP*[ÜšÓRºgâ¹%=%jD„Ñ«©åG¿ø¾äpÎÖœìÛi2Çžƒ¾°)–j1FR÷Àw™¹Ûâ«KŠ&©/…ö…ÄpvòcUy€x%Ó²ž¼}ì`Ë}õ MàÂfL>£ÙÙa#+¾“3Žca°™˜ ù좕Êî§"œùý“4s½.á![sâè>q_yÆF!,×#úº]z»\|W¬àP¯ÏövE·x~oÀëG7ã&'DZ%Ó«¥Ã…m&{x¢+æ+Ïîö‡{É*ÅðîSÒ¾>ãÁÀ‘Ânh%ŠÅ*d=…ïjŒÀ·áæø{&n´˜¯D´šZ÷ªÐ½5¡ÍO F°žßòœœ¯Xª§/e=›V&g¡§ö¸Y'p3ÔBáÇë†ÛM=r<åH•ž·ä {Eü=µ‹µ” Ò°¸WƒåªÔÖæÔèï³É6FàÛ«†k%k퓲>J„:l 'BÞw K¡µAÓ«XI…ã/ûô<å!ÀÄ„¯ÝؾFÈ‹HÎNLjlưP…5M­°¼ñ’Ë®í­¾ù-áóïË¡~ ÃDR[„ª@¥ZXÖ}N€a{§¦kKJqé¦áËŸa¬×üÕŠo3D*(ª¶`ºÖD²J)AD¥‰K¦•ƲÕ\¼ž*A?dÊTj˜i„}ù¼ a¼ÖXDÐJai°5ØVŠ»U‹‰?5ß]ƒ™ùÔæÜ¡R310^*:¡Ö…±Z“÷êMÙ×¹Y¡ÔÆz$IL¥ÚB+MÖ÷IDÝWW¸S6,‡2Œ­5ÈÏ#ª'j¤´hjÊ4Éx]AÆõļv$àø ýû9ý=$qÈ_s­Z+‘‘RÑ)o˜£K »øË´Ž’¸ï ÁJ ëz´Œ&NÒÚ^ ßâ¨ÁÄM‰ÊuU}èØ|ú]É‹pnk6:y [2½Çq°-ØÕŽ SsÆGM&K•[I}®ê\ΖŠ^呃~Ï;’ò¬d¸3ß*ìïl8;6ÛÊ:³ó±\ûÇg­©0Ö# £¥¢·üD«©pJÚEaÀ¤mš‘T§˜i`d¬TtËÿkç­žî7Åa«i‘*ÈB©èó‰áÕg` ýcbø táéëŸÞ}þÃÀÅÁ ö3ЀýÀÀýôñ7ƒ²Ü€‚pýцmÇ?0üÿýŸÁßFˆAR’‹áý÷¯ ‡/È V 3¾ýf`øõ—‘ ª €˜`€Œz÷ƒáÔÙ ?€,tùüm…¾~ê‹ ¦:¼ Rì †JÜ@ƒ^ ¸ œu9.ßãcˆd°Ðâe`gbd”æ`ˆpaàáaa‘ãfØ´ë%ƒ¢';Ü^€‚àeÄÍðƒ…A_—ƒ,ŒA†Ô IP |üúA1L’AûÃç/?ø8Ájnƒ>ó_?¾ÿ:ñç?vVFfV ß9™˜~ý`xyb3ÃK´¤ @pþþgd`ý÷“AŠáã·Àh½ùá0@E…Ø ´¹¾« Ó d\XXÈ @p˜™™xøøÁlaáÿ FŒP™ÿ ÿÑúå+ÂßPÍ`7€……gŽcêåââÂÐ Ä$ k€žã÷´ª¿—IEND®B`‚qgit-2.7/src/resources/image.png000066400000000000000000000013701305655150700167270ustar00rootroot00000000000000‰PNG  IHDRóÿa pHYs  šœgAMA±Ž|ûQ“ cHRMz%€ƒùÿ€éu0ê`:˜o’_ÅFnIDATxÚbìïïÿÏ@ QPPÀpéá_†—ÿ0|üú‡›“‘áøÍ/ 7Ÿý`øùûÃ×@üó/÷_¾ÙŸ¾ÿfx1ÇŒa„ xƒ@(„ E+tþ3´ÿf©IPñ½‹ØßRÍv# ȪJq§ Òa…MýGÁ `ýÿ•á÷¯ß <<Ü ÌÌ áÿ`Í~ÿeø¤ÿÿb (#dB ¸W®^gxõꃒ’///ï_¿~üøÁÀÂÂÂÀÈÈtÁ_° Þ~caøðé+ƒ£X@Á 011aøöíÃçÏŸ¾~ý ¦………Á†ýùý›áÛ÷ï ?~`xú‘‰áÝ»ßpÜ€³gÏ2|*úóçØ === W˜Ö¬^ÉðèÑ#c3EE]±Ÿ ÿDàÚκsçØÙ ØÙÙøøøŽ=Ì0cÝ=~e†K×–3ä¤1prqƒ]Ä„¯ ‚È ~~5 y5MIq1`ìüg¸{÷.î]»àêî‚ß@‚0HãÛ·onݺt²CY'0p_Ux]ø›áÉ“'`—Â@Á `bbc¿AøÜ¹sà°dàáêõë×ù¬n€±±1Ü üóçO0ýéÓ'¸˜¤¤$ƒÀÊ•+Á4@Á ’’B PÂ… (€bèàÁƒ ááá`qP2.,,ä ¸\\\83 ÈdgC5³‚ €à€üprr"k†§e€b  5ƒ@€̸|qºIEND®B`‚qgit-2.7/src/resources/mail_get.png000066400000000000000000000015721305655150700174320ustar00rootroot00000000000000‰PNG  IHDRóÿaAIDATxÚ…“]hU…Ï™Ìþ&û—"Á ÓÒ- [šBPPDM[ÐXÁæÁVXØÔEˆ4 4?¬!Mœ Ëf·“ÍNgwçÎÜëƒ(ˆçý;çå|ÿ“sçfBžÇ²òiÇqG,«ý¦ãЯ''_á ýxáÂwŠë²ãŠ"Ž÷õ¥žÒ´Ècår,.nîî¶¿`ÿ­àêÕ‚àyL%„<)äíd2r,—{¼;€s@ ¶·«/íì˜(þ¥`jêûçüÙhT}YÓ¢Ï tû’ÉD‘Àó8j5>Ÿ„LF ¬¬”^pÈôôŒñªêK§Çu=Ñ´0DQpÎá8 ëëe„B \—ãÚµür¹üð…7Þ0ÆX¶«+ðÉðpÿó‡éM‹ü €çqPÊP*™e >Ÿ€ƒ{úãccc—ÁuY…lPʈiÚØÜ¬Á0LضÆ~_o6pÎáy ”2¤Ó‰@ Ðñ c,.0Æ6 ÃÌ»®çrÅ•öa=jc~¾ˆ¹¹"ªÕLó€R®ë!TÐÛ¢ÔͼŽà@ìÈ[UÅ8ãtÚA@wuœÉEŸp‹ ã8p ‘H„Œq¬®–pûöÏ×E áDµgçC'ÛŠž8…áÃø#ÞÁìjÇžÈ! !™ŒÀï—@©Û¦ØÛ³Q,–ÒT¼'ö‹qI•ñgp4r²"£ž®ãÓ¥‹H¥ÂèíC–E¸®Çq±´´EóùÅ5Ëj½+Éòàˆ>UR!A‚Ä$Œª£P:Üüå&v„å,«…û÷Klaac{}Ýø’R:U(L’(ŠÈÅrO£bUP:<„¼•ÇŒ8J=l—k˜Ÿ_uÖÖJ×kµý/ãs…„ R{¯}ï’qiP j8Ùu‘0Û˜ÅÄÆÂf?ŒÆòòÖB½n]l6»„ ™ÏÀÿø‰DjäãÖ½Öå+þ+z@ *Fq~ã<ÌŸ¼ÿ×§Wæ*+“Œ±onÝz§úOÒ¼ UË/Á÷u]”ˆ„‡ZÍŽJjº{?ûùݯ>[ú/Ý;ú€ºÖÜIEND®B`‚qgit-2.7/src/resources/mail_send.png000066400000000000000000000015751305655150700176070ustar00rootroot00000000000000‰PNG  IHDRóÿaDIDATxÚ…“MhuÅß|ìdöc²›Îfv&’nH¨i"ž¬Ú”ÚƒR%øq­(„F‹ÖƒT¡b{(TÄJ©!§„ öRÁ")YXjš$¦a!•eH7Yg›Ýæc3Ýu6™ÿüÿUAüz÷÷~ïðž€ÿÑéÓW5JY8¬ñýàÅz}ç]ß'ßž?ÿ:ùߌçÎ]Sƒ€ ¨ªôFWWêeÅÛËeWXXXyimmç{Þß.^ÌŠ”²ˆ ‰¢ð¾aÄŸÚ·o·™LFÁ9 IŠÅõ—ïÞ­ž`ÿ%`tô‡çüÙD"òše%ïÙc† Cƒ$  ”ccÃC($£»ÛŠÞ¾]: à#ÆÆn¶0Æ_‰DBƒétë@[["nYÍ$À9‡ï3äóehšŠ àÉäÊåÍ.]vDÆXÿ®]ÑÏìy¾¯¯-nYñ?Í@)! ¥RŠ"#ÑÛûpc|ppð Q vO°DªU…§ ÏóÁØz£áƒsJaH§[£ÑhøÆXRdŒ§š¡”¦Ù ]Àó|ÌÎÚ˜™±±¾^Cµú+€Š  ˆÅTttèû ú¥ÉÉqúÈþ'i¥V~NnÙŽÕÉÚ­VtuZpÝmLOç`Ûe¤R 0ø~ð{3)×d oÁœ“¦ü¤Ì>aù¦ {Kw–Л샦jhi‰A×5FMM2¡ð<‚­-¶]JK8„O”þÐð;O‹ž|ô$*á ÆÊã¨-×°[éÃC¦Žææ0€ôFÃÇ­[ËäÆEÛu½3²Öª½}bà„:dÁP œí9 Žk?fpØ{íH†z}‹‹%67·TÌç¯ !£Ùì)GÔ“z¼GéÁ…¸ÔÅÔæÄ€I‚JáûÅâ&'çý©©ù‰|þ—7cŸf³§ŠS. §âJÇùqŒoŽc¦:ƒ¤obǮޜ¯årËs®[ÿ¬Ñ仌ÌÇüÈØÄWbAüPß«Gî/ 8*Úîì¥×s+?¯®Ô¾dŒ}wåÊëÿt:A<*ª®%ÌıN«3µzoÕ‹ÙÝAÇýÇGÔíä7—/¿—û¯»ÿ…—|b'"IEND®B`‚qgit-2.7/src/resources/minusonly.png000066400000000000000000000002241305655150700176770ustar00rootroot00000000000000‰PNG  IHDR;Ö•J[IDAT(ÏÕA À GèËúò~¢Oñf»^L!h‹] ’a àG’ˆ·1°ô'~Y àvÏÔë‰æÿ^{žy$Á†cÐp{á6ûŒÀ$§ëgyà³YôàTÁòˆ¥/êLIEND®B`‚qgit-2.7/src/resources/misc.png000066400000000000000000000006371305655150700166050ustar00rootroot00000000000000‰PNG  IHDRóÿa pHYs  šœgAMA±Ž|ûQ“ cHRMz%€ƒùÿ€éu0ê`:˜o’_ÅFIDATxÚbìïïÿÏ@ QPP€SÁÿÿÿþýû§alNNN† &0 >ÿþE1Æa &| c!Èâ0@LÄh†i€Ñ0Ã@ €˜ðùÉýúõ ® €˜ˆ±ÿþýÅ Ä„/Ä‘ ñaš™™™áz 0èB„a"k€‚ðùóg&&&FFF¬¡’Ã% ‡.ÜPCAäd—‚@Á @w2ðýûw†½{÷bÈ ²Mø €¹"<<Lƒ’qaa!?@‘l’fv ó@Á àààÀ›ë`^„j†›@,0Ab²f0„ö)_âŒIEND®B`‚qgit-2.7/src/resources/next.png000066400000000000000000000015771305655150700166340ustar00rootroot00000000000000‰PNG  IHDRóÿagAMA± üa pHYs × ×B(›x!IDAT8Ë]“_h[eÅß÷Ý›{on–¥fkgëì¶Ò¤*+Åvêœ8ŠÙ„†0&êÔ|« ‚àAôÁ):”A­n…!Šh§Hm²¦e¦mš¶Išæÿͽ7>tÎÎózþ<œÃümÃ]ÀSÀ=èú~\wFc˜>ba$±Y.6%p#ðFï¡~óÉ»Ù×} 5×ç§é,_Œ'øãëŸ+Ôj§3,Œøÿ´ à»pOçÑ·¡oWˆµ²G¡æQs} Mpq*Ç«o^¤8°0ÒP„ï{Á¾£ó¹™Ñ£´F r¥:·†5fæ ¤Š™’K¦äÒdkÜ;°‡±ßWwûÙ\ŽÂжá=ØöùÑ÷èMAdÎAW‚ÖˆA{³Åìü:óŸbͧPõi4@n¿27ˆ9pNÇî:ØgÝy›Ml©ÊRÞáüd–‰™5„<Ü×B“ôHåRy‡Å¼CW«èêG”ŠÞÿÒñg÷wKMãÊ|‰ÄJ•dÎá×d™ˆ»›ƒôÜ"þwžéLBÍ£ìø\[÷¨Ï%Kšn{[¶ùàÇeê^ã¦E_ÿ>MÙipèî('ØÉ¥w§ÈùµY‘-”Cv¿¦,Cÿ+Ua2Yd‹©n4þïDÕë¡JJVò5f+‚|Å¥Põ0@] hjba¹x¸PÝ(I ’ %¼ýx;õnÃó|N~8Í/éúØ^¥«ß¤f—çÖh ‰tÁ™'Úyf Ïóyñã£ÓE‚–FÐl *L·†f“R7ŒsËW—œ°! ›v@q|_3Gû7Ì/ç««eB¦ŽP„M­‰¾’­è†qV.Ž %œÊ$Ò´Ft–"•wÈN}ãÛ¹*!KÃ6$aKÑÑqR«(!N/Ž Í €Ž§¿À{Gdpgg nª®Oµî`êK—(ÉXšRzíp0ñÙ£gê>öž—šxmÇ®íVSÔ"dëH!X/ÖÈ®VH_ËT}×x'vö!ÿæ7^Gωñ`Ø ì•J„}¯q ¸ |þç{ƒñÍúÈÔD÷Ÿ¿T„IEND®B`‚qgit-2.7/src/resources/ok.png000066400000000000000000000012251305655150700162550ustar00rootroot00000000000000‰PNG  IHDRóÿagAMA¯È7ŠétEXtSoftwareAdobe ImageReadyqÉe<'IDATxÚbüÿÿ?% €˜(ÄÂ8Q–áÃc†¯@Þ7 þÄ@2@ÌĬHl(ŸA™!™™™A €Xþ2lj¨ oÇkȧÿ¤€dP}Ú¯? ?YBˆ‰é/ÓoVÖÅ ¿qjüÅ $ËdöIÊH6]ùˆá'ƒPì"@1ýÿô•°–°0‡Ç"† – ŒHš3ˆ]˜Á"ÍrTÚWºƒ_–_ýÕíWۚÀꞀ”ËÿÿO¿ûñîS“ Ã;†¥ B€â×€¶&0È3$0Z1š³™²1|8ùáëö¯«˜Ò8>Àì F†1 Âí  F ¯€"óîø–ÁŽÁŒÁÈâ @¼…aC/ÃGp€r000r10 0`^1Üa8Åph€:P"œA™ ˆ%Á^``˜ ²S ý ì •@Ûÿ¢@1ýÃÀð…á4Ã% $Èì@Ì ÄxïjüÏP†M3 Ãg ù—áà³õ€ÞÅó°³0ÜfÈ6›Or °¹n5]ex4àØ¿ïU ôó|i €XÀþüÄ?Ž=L Ï^5ÇSÝ”(ň\ `…ÿN~Î`Îð /, {Àb,HI™f†°l€b¤47Ź ÀÃQ‹ôÓU!6IEND®B`‚qgit-2.7/src/resources/openterm.png000066400000000000000000000016021305655150700174740ustar00rootroot00000000000000‰PNG  IHDRÄ´l;bKGDÿÿÿ ½§“ pHYs  ÒÝ~ütIMEÓ $ååÖFIDATxÚÕ”½nEÇ3»Žm¬(N‚ã\äQPÑÐòPXDT<%„G GT<•h+‰†ÆBHQ"‘8E÷Ú÷ÞÝ9³»öi)8ÒÑ™9«ýŸÿùø$|ûÓâ}Sÿ<%E’’;%'#wJJ†$!u†&#µ™œŒ”MJîŒÔ)¹?K¡jßÔ×·ãϼ»¹³»p70wÇ­Ü¿ªcª V¬ª#戂šóøñ‚ï¿ûãÓzç;·#’œ_;g6o‰U ®Uìµ ÄuUŒTÄ꿹ØÚ ܸ¹Åíý›üøÃöN­YÁáåiËÇØÝþ‹Ù|Æ‹çÏÉ"wÔ”œ3)uäÔÛœH©hΉ½½=¾¼ÿöÆÔ’w0wª¨¼óö>wî¼Çt:åôt†&‚æDjRJ´m;Ú®ëJ€nAÛ,`»4/J¶ÒÅ`2™pxxHŒ÷â쿉»÷ÿÌ¢ˆá1€HI àøø˜ªª0³+Ü}Msθje¢ŽŒ!u-‹Å‚étÊÑÑ9ç5`3[Ó!@×uˆhŸXSa¼Q)šgk7 Ö@Ep»hî fj-¥UÓ°\.Ç4_g)"k.ë ŒSyÅBµ2Bðev#{Ɉ^0ê[UU¿©—æØ€:rþÙGe±ûf ›µ¾màøÚ·ºÞàî[<{Úß_½jùóiËþÞ&»oÞ% XÁÅèßæÁ^ò êÞ§ïðä ¼|Ö²Z$‡÷~¹ëƵ¯6êHÎÖ§VÀ óáibô¯/ùÚF˜ýÝ|ÍÿNþµWJšï$âIEND®B`‚qgit-2.7/src/resources/pencil.png000066400000000000000000000005771305655150700171270ustar00rootroot00000000000000‰PNG  IHDRVÎŽWgAMA± üabKGDÿÿÿ ½§“ pHYs  ’ù¥tIMEÒ5#Õ6@/üIDATxœ¥Ó1jÃ@@ÑoËeP¯ZP™ÆD] 'Hk’ƒè(:‚. ˜°jC‚ÝJµ°Kƒ ˜’`-'Ö.žfvyÌ ;਀ªˆ (Á]È=áÚI'>ÒÔ¾ À úëÍ"ä‚,BKHæ9"ï@ôBÈøºÕÈ'pú+HUµª*úíÖÙŒêºV€(ŠŒqF® 8Žiš†¶m$Ëœ€Ý’”eIQ|; ! ÆÍó\“$Q†=z^ âÆr®&D†<Ídúg`?æîV%,@`üø1lðãXúÁ˜Ckàa<{Sü¿kž?qt¥õIEND®B`‚qgit-2.7/src/resources/plusminus.png000066400000000000000000000003321305655150700177010ustar00rootroot00000000000000‰PNG  IHDR;Ö•J¡IDAT(ϵ‘A Â0E_ÄC¹öRnÅûx½„7°ºi‹.Ü%1 ™ ~Ì<2ÿÃÒa:àˆ«µ¬h)è1Ô†£žEðÌÏnâ1—¨ß+°ÉÞ8|¸uj²¢è'p/jª/|«À#ðÈjѻԡÓÑO¿®*jX²$íÐ^Ó<øŸR离i[Š=¿tJoÜ¡â$Á—¢Ñ‚}¿x®HÎú$¿IEND®B`‚qgit-2.7/src/resources/plusonly.png000066400000000000000000000003061305655150700175300ustar00rootroot00000000000000‰PNG  IHDR;Ö•JIDAT(Ͻ‘M‚0F_‰‡òfn‰÷ñ \Fd£D÷´Ÿ 'RI[¾dÒ¤3o~9Õˆh .RQ’7˨ ³UðLe÷3c,Ù¼Wàý 8¸Ã7ø9 ÀxÝÄgá[îgä ¹¶§:!Zk½Yzª`ËÒšmûò™æaÏ>’$ýó&É ¶W~#€íºå9˜IEND®B`‚qgit-2.7/src/resources/previous.png000066400000000000000000000015671305655150700175310ustar00rootroot00000000000000‰PNG  IHDRóÿagAMA± üa pHYs × ×B(›xIDAT8Ëe“KhuÆóŸ™™ífcš´$Fó Y(QImU¨K¥"è¥Mã!<´Ч" x‹ ‚A¬‚"ˆ©•HØ‚ÝVCMÓŒ!Ýl–m÷w&»3³óð ´©~çïßÇ'ñ_ôÌ G½¨êcA8Î9àc ³Övº´M(€ÑoŒ=¹O?òD–‡³÷à?/U9»`ñÛ7¿4ñ¼ÓÀÛf£;=3ð]zdèàÜ[O3Þ—b³b{!^¡)€óWk¼úæyœ«Ë ÀA ³± @úÀËæž¡V枥;£±^ó°n¹øAÄXo+_'o‡´› î`þJ¹?ªÖjØ?\é™À4¿œ{JmO*äk>u7dË y<Û†¡)ÔlŸÅ¢‹ãÇÄ1ˆÎ–/¯N ïÿTŽ?xhÜxà^“¥¢‹D”Sãè ™µRƒ¹_+ÄZâv]ÃÝ&Òð`"¾teJ‘Uuô©\Î7XÜhÖež¤SW°Š¯}ƒ@M€ëß5V¦§“úïê^E5“c»v&9óÓM‚0æ“çIé Kë6§¾XÅ•U õ¿µÌ)sŸ"šúG±I.ï°C—i…ASñbìàNrƒí…üÕ °Ý]K´DB‘/n:ØnÄÆf‹É3+TmŸ‘ûÒ¼{¬Ÿ­­ë5ŸUŸµŠGÅ B0à U¾$]»¸^Øäþ¤&X.»ýà:UÇ硾4gO³ÓÐT C$5A[RF<]ËÉ™ì‘B£îžìêï’ÃXBµfÈ–Íá=môu%éM VŠÀPeLE¢e­7Ò ±1?mÉpªb•èΨ¤ S¬Õ|&?\áVÝ#·ZÇÔiC¦;£âËÈ’tzc~úO `pò+ 8gîÎLôí"ˆÁ "ÜÖ?…êªÀP²ùk%¶J›ç€CÖgÏÄ·Ï”=þ­^ŠôÚî¾N£½Ã eªI¢îxTËMJk7 ¢W€w®}t8ºûÿbäÄ 0 Œ£B–ÒQ\>_|oby;ÿo7)Lûð®uIEND®B`‚qgit-2.7/src/resources/qgit.icns000066400000000000000000002007011305655150700167600ustar00rootroot00000000000000icnsÁis32¥‚ †L… $D" ‚  LFD?EFL‚ $F!…Bƒ *%/?-%*€ jüèéæéçú\€ 0m_ghedm(ƒ , 0IBG‚ \wRcic € +p ^2j€ 9.9=A‚  … 99UNS‚ ZuPafa € +p ^2j€ ‰ÿ~?RYÿI5)O“J*4sÿûþþþûÿc:~fy|us~.„#ˆÍ¹ÇHÚÿ0Àðõé gÿPÝu%ÿLòZ3õ‰qÿ55ÿiîòˆfˆñì:ÜÿüÿÜ9il32׋–ECB “€A?> ‘B@?’  A@? €ŠGA€BA€@BAEˆD?ˆ@?CˆC>?€@?>BŠ   B@?B@?“€B@? ‘DBA ˜€ H½®€±°€®°€±¯º3ˆZìÙÝ€ÞÝÛè?ˆ]öâ‚æƒçäòBˆ )lcffeedƒa`f’… KPhk‚jnŠ3joZ dfgfdfej‰)odj_ dg`fkgZŒ_gh]) cm, Ijj;‰$lg\ bm)Pjiˆ8lk1hs+,kk/ˆ=lk$ $jl7ˆ;kk,‚,kl4ˆ+liPPik%ˆeek?‚?kfb ‰=obl\:&"&9[lbo9ŒSncgm€lmgcoRFklg€egllE D[cdc[E†‰ 976–.ÎÈÄ€-ýº€,Å¿»Ž/+,.%Nÿ¼?'-,,. ˆPÒ€ÅÀ¿Ä€ÅÃÏ8ˆN̼ˆ¿½È7ˆLȸ€»¼€¿»¹Ä6Š ?Ä¿¼/,Å¿»‘€-ľ»€‘-ËÅÁ– ,+*1-€.,€%-€.-0 ˆ=89€;98<ˆ@;ˆ<;?Š „“… HNfig€hglŠ3joZ dfgfdfej‰)odj_ dg`fkgZŒ_gh]) cm, Ijj;‰$lg\ bm)Pjiˆ8lk1hs+,kk/ˆ=lk$ $jl7ˆ;kk,‚,kl4ˆ+liPPik%ˆeek?‚?kfb ‰=obl\:&"&9[lbo9ŒSncgm€lmgcoRFklg€egllE D[cdc[E†‹–RPO ‘€MKJ €‘NLK  NLK€Š SM€NM€LNMRˆQKˆLKPˆPJK€LKJOŠ  NLK NLK ‘€NLK €‘PNM –($€%$€!$%' Š1-.€/.-0 ˆ3/‚0ƒ103Š „“…HMfigghhgkŠ3joZ dfgfdfej‰)odj_ dg`fkgZŒ_gh]) cm, Ijj;‰$lg\ bm)Pjiˆ8lk1hs+,kk/ˆ=lk$ $jl7ˆ;kk,‚,kl4ˆ+liPPik%ˆeek?‚?kfb ‰=obl\:&"&9[lbo9ŒSncgm€lmgcoRFklg€egllE D[cdc[E†l8mkKIH ;ÿÿú!<ÿü÷$;ÿÿú!?:;=1hÿÿûU5<;:>hÿûÿÿÿÿÿÿÿÿÿÿÿüÿIhÿûÿÿÿÿÿÿÿÿÿÿÿüÿIeÿöúúùûÿÿÿúúúú÷ÿH # !#Tÿÿû>"!!# ;ÿÿú!<ÿþù#;ÿÿú! :87UßÎÑÑÑÎÄÅÅÏÑÑÑÏÛ<hÿûÿÿÿÿÿÿÿÿÿÿÿüÿIhÿûÿÿÿÿÿÿÿÿÿÿÿüÿI0uzzvwvmmmmmmlr >¶Ä#ûÿþÿÿÿýÿ;ÿÿß ûÿþýûÿüÿ9fÿøÿà úÿðùþÿá0åÿÿäh"÷ÿm´ÿÿ”Wÿÿâ!ôÿfÇÿý2Šÿÿ{ !÷ÿgmÿÿs–ÿÿZ GNZÿÿ†ÿÿnnÿÿ€hÿÿÆÆÿÿY$öýÿ œÿÿï•ÿöÿä’`V_áÿ÷ÿŒÌÿøýÿÿÿÿÿýøÿÈ °ÿþþýýýþþÿ®C«âøúøã­Fit32!ÿ²+'†()"íIC…DCF:íC>…?>A5íD?…@?B6íD?…@?B6íD?…@?B6íD?…@?B6íD?…@?B6íD?…@?B6íD?…@?B6íD?…@?B6íD?…@?B6íD?…@?B6íD?…@?B6דD?…@?B6–ÔD?…@?B6Ö“'C?†@A:”» •DB‰@A“DCI+» –?Š@“?>C'¹ ¹@?D(¹ ¹@?D(¹ ¹@?D(¹ ¹@?D(¹ ¹@?D(¹ ¹@?D(¹ ¹@?D(¹ ¹@?D(» –?Š@“?>C'¹ •BAŠ@“BAG)»”65:Aˆ@>“65:"ÒD?…@?B6íD?…@?B6íD?…@?B6íD?…@?B6íD?…@?B6íD?…@?B6íD?…@?B6íD?…@?B6íD?…@?B6íD?…@?B6íD?…@?B6íD?…@?B6íD?…@?B6íD?…@?B6íC>…?>A5íIC…DCF:ï ‡ÿîŠÿÒ¹·´Är¹"¹àÝ ¹ÖÓ冹!¹ØÕ燹!¹ØÕ燹!¹ØÕ燹!¹ØÕ燹!¹ØÕ燹!¹ØÕ燹!¹ØÕ燹!¹ØÕ燹 ¹ÕÒä…¹#¹çã÷¹ ¹QPW3ÿº»ÛžÒ÷ˆ ›Ë€ „Wje™fdm&Ç 0IZei‚Wje™fdm&Å+Sjmlife‚Wje™fdm&Ä+\mjeddeff‚Wje™fdm&ÃSmideƒf‚Wje™fdm&Â)gkde…f‚Wje™fdm&Á=ofd‡f‚Wje™fdk%À>odeˆf‚Wje•f€dfgs)¿,á°ÿ²-u…wv{eíLÚÉ…ÌÊÓ­íFɹ…¼ºÃ íH̼…¿½Æ¢íH̼…¿½Æ¢íH̼…¿½Æ¢íH̼…¿½Æ¢íH̼…¿½Æ¢íH̼…¿½Æ¢íH̼…¿½Æ¢íH̼…¿½Æ¢íH̼…¿½Æ¢íH̼…¿½Æ¢íH̼…¿½Æ¢×“I̼…¿½Æ£•ÔH̼…¿½Æ¢Ô “HJ@uǽ…¿¾Ã­“HGM-¹”ÌÍǾˆ¿Á“ÌÉÚ€¹•¼½Š¿“¼¹Év¹¹¿¼Ìw¹¹¿¼Ìw¹¹¿¼Ìw¹¹¿¼Ìw¹¹¿¼Ìw¹¹¿¼Ìw¹¹¿¼Ìw¹¹¿¼Ìw¹•½¾Š¿“½ºÊv¹•Æþˆ¿À“ÆÃÔ|¹”¢ ­Á‡¿À»“¢Ÿ­eÒH̼…¿½Æ¢íH̼…¿½Æ¢íH̼…¿½Æ¢íH̼…¿½Æ¢íH̼…¿½Æ¢íH̼…¿½Æ¢íH̼…¿½Æ¢íH̼…¿½Æ¢íH̼…¿½Æ¢íH̼…¿½Æ¢íH̼…¿½Æ¢íH̼…¿½Æ¢íH̼…¿½Æ¢íH̼…¿½Æ¢íFɹ…¼ºÃ íLÚÉ…ÌÊÓ­íMG…HGK=ÿíŠÿÔ¹0/3» ¹:9>$»¹87<#»¹87<#»¹87<#»¹87<#»¹87<#»¹87<#»¹87<#»¹87<#»¹87<#»¹76;"» ¹<;@&»º ÿ¼¼ÛžÒ÷ˆ ›Ë€ „Wje™fdm&Ç 0IZei‚Wje™fdm&Å+Sjmlife‚Wje™fdm&Ä+\mjeddeff‚Wje™fdm&ÃSmideƒf‚Wje™fdm&Â)gkde…f‚Wje™fdm&Á=ofd‡f‚Wje™fdk%À>odeˆf‚Wje•f€dfgs)¿,á°ÿ²3/†02)íWP…QPTEíPJ…KJN@íQK…LKO@íQK…LKO@íQK…LKO@íQK…LKO@íQK…LKO@íQK…LKO@íQK…LKO@íQK…LKO@íQK…LKO@íQK…LKO@íQK…LKO@דQK…LKOA–ÔQK…LKO@Ö“.OK†LNE”» •QO‰LM“QPW3¹ –KŠL“KJP/¹ ¹LKQ0¹ ¹LKQ0¹ ¹LKQ0¹ ¹LKQ0¹ ¹LKQ0¹ ¹LKQ0¹ ¹LKQ0¹ ¹LKQ0¹ –KŠL“KJP/¹ •ONŠL“ONT1¹ ”@?DMˆLJ“@?D(ÒQK…LKO@íQK…LKO@íQK…LKO@íQK…LKO@íQK…LKO@íQK…LKO@íQK…LKO@íQK…LKO@íQK…LKO@íQK…LKO@íQK…LKO@íQK…LKO@íQK…LKO@íQK…LKO@íPJ…KJN@íWP…QPTEï ‡ÿîŠÿÔ¹&%)»¹/.2»¹-,0»¹-,0»¹-,0»¹-,0»¹-,0»¹-,0»¹-,0»¹-,0»¹-,0»¹,+/»¹0/3»º ÿ¼¼ÛžÒ÷ˆ ›Ë€ „Wje™fdm&Ç 0IZei‚Wje™fdm&Å+Sjmlife‚Wje™fdm&Ä+\mjeddeff‚Wje™fdm&ÃSmideƒf‚Wje™fdm&Â)gkde…f‚Wje™fdm&Á=ofd‡f‚Wje™fdk%À>odeˆf‚Wje•f€dfgs)¿,á°t8mk@<ªŸŸŸŸŸŸŸŸž¥‡`ÿûÿÿÿÿÿÿÿÿýÿØ^ÿ÷ûûûûûûûûùÿÕ`ÿûÿÿÿÿÿÿÿÿýÿØ`ÿûÿÿÿÿÿÿÿÿýÿØ`ÿûÿÿÿÿÿÿÿÿýÿØ`ÿûÿÿÿÿÿÿÿÿýÿØ`ÿûÿÿÿÿÿÿÿÿýÿØ`ÿûÿÿÿÿÿÿÿÿýÿØ`ÿûÿÿÿÿÿÿÿÿýÿØ`ÿûÿÿÿÿÿÿÿÿýÿØ`ÿûÿÿÿÿÿÿÿÿýÿØ`ÿûÿÿÿÿÿÿÿÿýÿØ`ÿûÿÿÿÿÿÿÿÿýÿØbÿûÿÿÿÿÿÿÿÿýÿÙ`ÿûÿÿÿÿÿÿÿÿýÿØ``````````````````````bUœÿýÿÿÿÿÿÿÿÿþÿç``````````````````````_g<'ÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿûÿŸ&ûûûûûûûûûûûûûûûûûûûûûûûûüÿÿÿÿÿÿÿÿÿÿÿÿþûûûûûûûûûûûûûûûûûûûûûû÷ÿ'ÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿûÿŸ'ÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿûÿŸ'ÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿûÿŸ'ÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿûÿŸ'ÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿûÿŸ'ÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿûÿŸ'ÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿûÿŸ'ÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿûÿŸ&ýýýýýýýýýýýýýýýýýýýýýýýýþÿÿÿÿÿÿÿÿÿÿÿÿÿýýýýýýýýýýýýýýýýýýýýýýùÿž'ÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿûÿŸ!ØØØØØØØØØØØØØØØØØØØØØØÙÕçÿþÿÿÿÿÿÿÿÿÿÿùØØØØØØØØØØØØØØØØØØØØØØÕç‡`ÿûÿÿÿÿÿÿÿÿýÿØ`ÿûÿÿÿÿÿÿÿÿýÿØ`ÿûÿÿÿÿÿÿÿÿýÿØ`ÿûÿÿÿÿÿÿÿÿýÿØ`ÿûÿÿÿÿÿÿÿÿýÿØ`ÿûÿÿÿÿÿÿÿÿýÿØ`ÿûÿÿÿÿÿÿÿÿýÿØ`ÿûÿÿÿÿÿÿÿÿýÿØ`ÿûÿÿÿÿÿÿÿÿýÿØ`ÿûÿÿÿÿÿÿÿÿýÿØ`ÿûÿÿÿÿÿÿÿÿýÿØ`ÿûÿÿÿÿÿÿÿÿýÿØ`ÿûÿÿÿÿÿÿÿÿýÿØ`ÿûÿÿÿÿÿÿÿÿýÿØ^ÿ÷ûûûûûûûûùÿÕ`ÿûÿÿÿÿÿÿÿÿýÿØ$g_````````_cQ!ØØØØØØØØØØØØØØØØØØØØØØØØØØØØØØØØØØØØØØØØØØØØØØØØØØØØØØØØØØØØÕç‡'ÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿûÿŸ&ýýýýýýýýýýýýýýýýýýýýýýýýýýýýýýýýýýýýýýýýýýýýýýýýýýýýýýýýýýýýùÿž'ÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿûÿŸ'ÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿûÿŸ'ÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿûÿŸ'ÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿûÿŸ'ÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿûÿŸ'ÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿûÿŸ'ÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿûÿŸ'ÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿûÿŸ&ûûûûûûûûûûûûûûûûûûûûûûûûûûûûûûûûûûûûûûûûûûûûûûûûûûûûûûûûûûûû÷ÿ'ÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿûÿŸ````````````````````````````````````````````````````````````_g<!('''''''''''''''''''''''''''''&*#=Øÿýÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿûÿ`9x¶àÿÿ'Øÿýÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿûÿ` lÎÿþÿþÿþ&Øÿýÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿûÿ`läÿþþûüýÿÿ'Øÿýÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿûÿ`2Ïÿþûüÿÿÿÿÿÿ'Øÿýÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿûÿ`hùÿûýÿÿÿÿÿÿÿÿ'Øÿýÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿûÿ`šÿûüÿÿÿÿÿÿÿÿÿÿ'Øÿýÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿúÿ^šÿúþÿÿÿÿÿÿÿÿÿÿÿ'Øÿýÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿüûüÿûÿ`”ÿúÿÿÿÿÿÿÿÿÿÿÿÿÿ'ØÿýÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿþÿèË8eÿüÿÿÿÿÿÿÿÿÿÿÿÿÿÿ'Øÿýÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿû®ˆS+.÷ÿþÿÿÿÿÿÿÿÿÿÿÿÿÿÿ'Øÿýÿÿÿÿÿÿÿüûûüÿÿÿÿÿÿÿÿÿÿÿþÿ\¿ÿüÿÿÿÿÿÿÿÿÿÿÿÿþÿþ&Øÿýÿÿÿÿÿÿÿÿÿÿÿþûýÿÿÿÿÿÿÿÿÿþÿibÿüÿÿÿÿÿÿÿÿÿÿÿÿþûýÿ'Øÿýÿÿÿÿÿÿûk]’Åüÿÿüÿÿÿÿÿÿÿÿþüÿe ÞÿýÿÿÿÿÿÿÿÿÿÿÿüþþÿçØÿýÿÿÿÿÿÿÿ'&vâÿüþÿÿÿÿÿÿÿþüÿ\oÿûÿÿÿÿÿÿÿÿÿÿþüÿö«J Øÿýÿÿÿÿÿÿÿ*Šÿýýÿÿÿÿÿÿÿýÿï+Õÿýÿÿÿÿÿÿÿÿÿýýÿ­Øÿýÿÿÿÿÿÿÿ'=íÿýÿÿÿÿÿÿÿüÿ¿<ÿþÿÿÿÿÿÿÿÿÿýÿòZØÿýÿÿÿÿÿÿÿ'5îÿþÿÿÿÿÿÿÿûÿT‡ÿûÿÿÿÿÿÿÿÿþÿî5Øÿýÿÿÿÿÿÿÿ'NÿýÿÿÿÿÿÿÿýÿÉÕÿýÿÿÿÿÿÿÿÿþÿ\Øÿýÿÿÿÿÿÿÿ'«ÿüÿÿÿÿÿÿÿýÿ</ÿÿÿÿÿÿÿÿÿÿüÿ«Øÿýÿÿÿÿÿÿÿ'6ÿÿþÿÿÿÿÿÿûÿ‡`ÿûÿÿÿÿÿÿÿþÿû*Øÿýÿÿÿÿÿÿÿ'µÿüÿÿÿÿÿÿýÿÏ–ÿûÿÿÿÿÿÿÿüÿ´Øÿýÿÿÿÿÿÿÿ'iÿûÿÿÿÿÿÿþÿû*¨ÿûÿÿÿÿÿÿÿüÿaØÿýÿÿÿÿÿÿÿ'0ÿÿÿÿÿÿÿÿÿûÿ_Øÿýÿÿÿÿÿÿþÿù"Õÿùûûûûûûû&çÿþÿÿÿÿÿÿûÿ‡ØÿýÿÿÿÿÿÿýÿØØÿýÿÿÿÿÿÿÿ'ØÿýÿÿÿÿÿÿûÿŸØÿýÿÿÿÿÿÿýÿ؇¥žŸŸŸŸŸŸŸÙÿýÿÿÿÿÿÿûÿžØÿýÿÿÿÿÿÿýÿØØÿýÿÿÿÿÿÿûÿŸØÿýÿÿÿÿÿÿýÿÙÙÿýÿÿÿÿÿÿûÿŸØÿýÿÿÿÿÿÿýÿØØÿýÿÿÿÿÿÿûÿŸØÿýÿÿÿÿÿÿþÿççÿþÿÿÿÿÿÿûÿŸØÿýÿÿÿÿÿÿÿÿÿ00ÿÿÿÿÿÿÿÿÿûÿŸ¨ÿûÿÿÿÿÿÿÿûÿiiÿûÿÿÿÿÿÿÿûÿi–ÿûÿÿÿÿÿÿÿüÿµµÿüÿÿÿÿÿÿÿüÿW`ÿûÿÿÿÿÿÿÿþÿÿ66ÿÿþÿÿÿÿÿÿÿÿÿ'/ÿÿÿÿÿÿÿÿÿÿüÿœœÿüÿÿÿÿÿÿÿýÿÞÝÿýÿÿÿÿÿÿÿÿýÿNNÿýÿÿÿÿÿÿÿÿûÿ¨žÿüÿÿÿÿÿÿÿÿþÿêêÿþÿÿÿÿÿÿÿÿüÿaJÿýÿÿÿÿÿÿÿÿÿüÿËËÿüÿÿÿÿÿÿÿÿþÿð ÝÿýÿÿÿÿÿÿÿÿÿüÿÎ''Îÿüÿÿÿÿÿÿÿÿÿüÿ«oÿüÿÿÿÿÿÿÿÿÿÿüÿð``ðÿüÿÿÿÿÿÿÿÿÿÿüÿK ÝÿýÿÿÿÿÿÿÿÿÿÿýüÿÍO1¶ÿüýÿÿÿÿÿÿÿÿÿÿýÿÈoÿûÿÿÿÿÿÿÿÿÿÿÿþüþÿÏk`ÄýÿüþÿÿÿÿÿÿÿÿÿÿÿüÿT ÞÿýÿÿÿÿÿÿÿÿÿÿÿÿüüÿÿüÄ“X*'Gˆµóÿÿýüÿÿÿÿÿÿÿÿÿÿÿÿüÿ¿NÿüÿÿÿÿÿÿÿÿÿÿÿÿÿÿüûÿÿÿþÿðÕÙØØØÙÕçÿþÿÿÿûüÿÿÿÿÿÿÿÿÿÿÿÿÿþÿ÷.ÿúÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿüûüÿÿÿÿÿÿÿÿÿÿÿüûüþÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿúÿxËÿüÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿþýýýýýýýþÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿûÿ±+ïÿüÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿüÿÕ4ïÿüÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿüÿï44ïÿûÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿûÿî4'ÎÿúýÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿýúÿÊ ¥ÿüûþÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿþûýÿ—RâÿýûþÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿþüýÿãLôÿþûüÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿýûþÿôôÿÿþûüþÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿþüûþÿÿö«lÎÿþÿÿýûûûýýýýýýýýýüûûüÿÿÿÿÙy9xÃóÿþÿÿÿÿÿÿÿÿÿÿÿÿÿþÿóÈ9?d“ªØØØØØØØ×Ü´šnKic08:¦ jP ‡ ftypjp2 jp2 Ojp2hihdrcolr"cdefjp2cÿOÿQ2ÿR ÿ\ PXX`XX`XX`XXXPPXÿdKakadu-v5.2.1ÿ 9¹ÿ“ÏÁ r£1Þ×U9¼H‹X,%t–`!˨¬÷´m±Ÿ¡ÐÊÕ^@)›Jý“Øäãù/eŒ,†ûcI#tRÿ*<ƒw'ÏÀx%‘4ÐEúî;,9åºPþþ¤Ý¾§Ý&¡k¡ãÏÀ %µù€´íNÒF‡"]±.Ék¢;b1Aì#…!Ã\®8ÏÁr£2RgÛO“q“läó|.Æ+töûqtȵGµ14ûËveòÒžaE'”ìãõ÷ë`O?(ìh$„¤C0ÒÇàc¬iˆIIKµ»R†\Ñ¥†×í¥UØU?wDÀåiàæLAav³u¡¬ëºšÜo‰k#Ð å;ÔØX¢Ö*MÏUq¸Îvì‰z=Üá|gýŽ0P³ÓxÊ Ûª ä–t+r3s~—%õq`5äñÿ/‘½g J«Æþ‹8½LAè3á)œfÈdº÷xöwO 43ŽQ®¢ÁõGà9Ú."}R2D"Ú´yÙÄhj6íUŽYÆ[8oßø6 99YC•‡ùläQEó´K#m\¾Å€!ª%JpÝžC¡eÅkøÏ¹(¼Ü9ÂÁõÇàMÚ@"}C&,.Í›7ÌsˆßòÊEV™ƒ­:Ð(ñÐðF Ž`rÞíí¼<%†Æÿ=.ù~è¼2³-è)Îa•GÔØ¿Sà/5V”Ý&süÒbø£)žû´aM¹´ñS×öô¬¼ÏÌâ~gÑùžIAÄ–Œšfƒâ¢}Àl°¥"W¿åÀF¥¬1‚ ºvÝrxfCrÚŠ+ëJEoL„løl0 Çï°X~sÆ Xš#€[£ ds´ÊMuÁg \ÞÝ+g€Ûæ`ÇÞ= ÌLiþ4Ðüß¿¿šg™uŸ-H¬JV™ñS‰í¤^ïFÀn£lÈHª¿X4J¯x½……ý5|ì…ç$£{ór¼wŽpÀ(ÄJÃí~‡ÛGÁöÐ@)'™•A•hæz?›0÷­¶T "'bÀ¨Y˜ºÂ¢‹‘¥Eáaç¹²;‚õ ö¥f.µÖ2Ú 5S9Á"gÜ«z5lö~ àUæ}¹]'÷Í4—ÏÁéC†zrÃ\wˆV¶:½ðʃ×å~Ñ%œú’\a¥gša¬ò-•~³¾=¾,ºÒéCRž÷3õ÷»@cJèýÿtãÓÖ’$àƒçLœBµx—†s+($m Rb¦"Wí>Ÿ˜X î·ö%˜°Õd<Äpå–A9Cá^Á¥3'Ðü¡Ù¦•»š@{R U…»_ý¦\n•ܰ—Q²VMúÑÝ·fÜcóág-Ãí=‡Úê6)ä DS>ÇÀB}4R¶}[Œ9˜+‡žÒa>yù8q¿ îË%Ê„–èÎÿ<‘°wö9Þ̇§NÙ¹ø,é|³º ¨š¢f=lÙà,|ØT×:\)êñµÑ–<Œ®ÍÌîê% ›æ¨©¡s€)|Ðjܧz¾n+ÊðY•Ó¡Há. éÆ 8ÌA¢'Ðü¡Ù¥*ËV%X¨_=kÍÎó¾œ4MuG驆}Ô RùµæUùõåÇíZvÔxyß'ÇáIÏΪ‡áJ)'™”ì)˜9/EaÌv¤^B¬µQøëTd·ª i»^Á«ã\ºÆ‰·cÿw@ç—X|É忏k‘Ö’©CªïîÄÍÛƒèÁuuÛ|ë —JÅá¡}ödÛi·ªô@诵t`U43P@þÒ$<£ek»§¼'S¿*gõbød@mZ6ä÷fKA£ú%J-Z™ÛáF‹Æ„é8ÔæÞ~Ì¥èclünéaè[TOÃtImx³?SR7õ G¡ukŠ÷½aHçBÏL–¨02ŸZˆË¿éKC¸±!‹}gòívk1vu¨YsMNsãÛ p“Ó…’ÌΈpyÑ•-Üþ‰NÅʰ)ûx¢ äÆ¿\$ÑjÃÔ-Ï}µÝS,/c9@%8g— òr J?‰”õmÞÚàf‘²aˆçÈxÎð ìs.§4(V¼bN…”ã/CO´å8°øI°Ç³Mtöî“ðr–~­"H žŽXv_ߌ(QöF>Gǰ¹…»§’ËÝÚ¡ÒhÊ•,‘w `¸g˜Y¸ÍðÚðLŽËÃÀÜõ9X^H+}וÂ8×ê³0LŠÀ`…ÚßwÈâ`o™¯XÃíÐØ}ºiÛoY0¢ŽnóÁT ƒí{zú½wsÿúÉ–À4E+¶š×½ô>&Y[t³ñÊ<“7øÄ‚üŽß ½èðá„L;ä€çYÏ-H4n“¹û0‚‘"ëQdn-÷bÞÆ å¬F9a½²p Mˆ(ÐÈ5j­SYMúåŽkFû–úÌ€WáÝê-þ?Ŭ¨8¢×²pWm(¸ð ÒP¹Š˜S7GƒÈ›;\  )žÛè2(`‚»*ØN¼ûVègÂJtg„ îânõ†¡ «O§g¯žo¡«pG#6csÓJ§~  l‚ŒR´8­…ŠI¸°OiƒJý,”9K.¬×òºä˽„ºÅ}_êO‡”Lf½H_•¦‹…ÀCàËT2¹¨Cj\DÈ)qûÝ$ªžšóaœæÝ‰²{¿.ñO·|a’Ñ+­;Þ:n‰ßÝa£É×€ÀoÔ =Y;t&3%c²!sbB ­”ò^“¼Hò¦ß1b¯ïyÃRBrEP ×"Óª#µ³Ï˜>(Äñ›ÂL¼ ¢øðü!ík±woò:Y% $† ìØyؼ¢©É×ß¶¬®cÄÛéË$©Åø ¼Žf>—ÿpüƒªþ¸b/)×ogëýpKk:A‹@RضÍ(uµCÄ—ïÄù/)Å–‹E JݺX¨Û4àáùë† P;A¸xÐ8z,"5Á¤5DóB«“˜îիˬn’¸Q°ya[¾¢[Tl±û^ÿÚzÍÞÐÝ"ÑSófÉšG$+Ê|ðù : Wϰ‘kCÄë7]ëõciÍ\ç¸jgÝ<ÔmAËê–¢¦»R"ã¸ádköåužÔòÔ 'õ]-hT³Hp"Ø·ÛxG2tƒ@½IˆíSIsE Éwëúûy3󓆫)Áñ¸·©CÜä×£º,MK½-Ê[VçÜ/Ùªõy:îyFÂ4ÜAä›èŠy³ …mAcÿ… 9öú}jaç¬z$Ä60GÚ%dÐydrNÃÆ’"(J˜Ä´¢×ù·åÃíO‡ÚËê4ªEB?°&j]é‡w8iUÈišî=óœ{5XO¤9ö1OozÒA q'_>M†:˜æ×‰n±©ÎJp¾ižš¸^>0^Œ®pÑh)´y%Ëc£ƒIåJ=Ŭtä`zkærœÛ…àq¬øm›¨¼?¾Œ¬&ulŒ˜½®QKåD/ó' ÀfyM»MZ,ÖÈÎW~Ëudãà¦赓Âö/Ò=+¶7ð*ð¬žë‹Ã!¸wÝ®Ã,ÌûªN¡¾)Á`é.5%ÎÃíZ‡Úÿõ!X„ ¢ŽnòêÊXžl+-ѼfD»°* ˜Tö Iê¯çûSχl£¬ üuË÷$šK¶QfïÀ°ú­FŒlõE¾÷,ãœð1ÓüUsÍi1W$)0c0_>M‡”Lf¾‹¡?_-š#úæLµA#„ô2€+¿å™i\‘¼‡CýÍ›8*‘2“~ÉD9±£¬Å/+¶sØI2Þk5“=Ô< p™ÍݱM‚,ÇsÀòYl @#’ƒ“`ߟoœÊ´MÏÏ}“õvÚÏxôÒàÖTŽp"ƒ}ÔˆZ©_ii»|×'oq–ýjHŠÈeÕDX'-¾x_Ü+;3úõ“Nš¥œˆô»wèN=(’ôé~ôzñŒú?Çá¨Só׈~&Y0¢ŽnòëGJ%²ƒq,+}r¿øÅ¥XÝ N[MÀnâÎ;DÕÜÔs'ù´ÂH^,¨róXAÓ3ó¡>:IÐc×ì‰(½nÒÙWØÈ¨7F!NÐÀ¬º]l'¦½!¦-g»6ú•;ÈMYò¾–ž­by â]®]ØÄÛJ×”o?7cëKîÑ%goWOX^° ™»{pçñÀ²œQž%¸<ÿ'½s?3ÝÇ4cVrt÷û;äGe ×ǃ$:R:Ë&†5—³W~˜n>$]b¬-ÿ@ùß›Î/Opü¦‡×ÎÑ*ßèc!”=&¥µUýÊÃf‹£…ᵌ€¹Ýëät‘ßéßË{žJ¥ò7uÌ{¿ù©š³Xúì€õT“=¸†¿xSnEè_¶–oôÑ©4ž!8¾&[cÀiÎ…ý`Jƒ)@Aº¯©è2»ÀJÑÈÈbY®0d·M0³H­ Jt©õÛ匯1›iŦ"ù^Œ- **¹Hõ9ƒøc[¸Vœž¥ œÍíØyýª›>5Å=bÍ} 9&] &ÛLr€Dl…¤«)XBY†KªÕ1aÈŽ!œ¿ªQgm~½·Úß‘aÀ­`Û#9¡Ç p¼pÁ°ôíô9ØœÿC ”¢MmÜáHµty×T—ðAݶqß7$µ9ÞdºL:NY¯CÞÏËP·÷Ôb”‘ jåöQg´¼¿OÛ"ó÷æ0ôš¡J»wão°Oš=Rçìì_Lµ,†´rÆVãMârºœãÒ½ë‚6ô‘å.}5€±ûé&w„%ó7|X'Kyý%„Zþ—6oÑE5ÿs<`ÍLÇöjÕévÛdߊN™~®ÌHb¬’`åXD^Ìßÿ-¹ÄÙhàŽbžL÷2þBV ×>:œÝò ±žqLĪ)Ë6OC4ï¨óUYáP(h%¶÷D_Yg0Â}D®†ËŒÖMKÙ†P%1¦hã©vš IöÙûAÌÃíÝ8}½CíÖð"ý©¤'/ò–\'·SQÔ‚õô{bf¢¯ ŒúË}¦Ük`ãöx:Z)=QËŽ\„Dˤ·û:Ôu°ßàø]äSsuÿ¢KÎnô`úè¤~Ð)²@ô ï%¤¸xYЦt£ŠmÀÏ®øj”yI³á$ÏÿM*é"'SEŠ$Æ'Ûãµ ’Ü|yøô™uþ„<×a_nþ§V R*‡åZŸRíå+†úuóDô,/$‰¢Ô‚Bíò÷‰˜Í°Æ*XŠïØ8U›ìkÉXAÛ¯}½™ÿ*öÇ«sˆ1Ýög¼…R;ñûNÑ¡™ƒõ”>ÄAËl„íÑ„xÍç>‰Õ^¥é‡g…W§4â¢~°Àäq[›%¼“™êª¡¢¯0ñß݈ª(eþ‰©¤š`NélŸ%ÀsÚƒ¾ðÚǺÇ÷{ƒþíèWK´$hðn mÔò(CH'íQj“› ¤ˆ`&9Þ] ß¾ö ÕÀV3%¥KÿC¼n ±nËÎk_nÕ¶N/áz3P±ºæqRÞf¬ø!mÛÃ=1sþÅ1öìÛÍe¤z‹˜]z'Z°c•nCyæÐdîq:Fp;˜&¼3à’ªRÚ"øgf±m)}¥Ô²}Åxäß/²ú³†+ø±>S la¬®õYÿR³1aö‚D3X©K¼ñËY¥MƒªÊ¿h¦f* ÌC} 6bòKY3ôÌjb@½ùÖ§~å©LGЦúÔl WÁŽóí—›Fžãœ>ŽiƒYôÚ—ª5M½n¢kzd¡‹Úiç’§•‚ Ú Y¹¾¬K|ùíTD-DÂlÝÑjPQú)Â>…1Ž'w’z3‘Vš:(iÕnJdó·¸ Õ"KF*$)®ÀW òSýxý²YA_=^jW êwž:ÕÜ‘\NB0ÄÓpèë|í±ãfðž f‰ô+@¸ØÛt5—aZ¶ë±Î’Ñ>sסì)ÛPPiµÀƒ¸|îȤYÍj'¨æû¡I±>bE£ŒûC!¶gísç“ÝÈÔ@ö'¥¾±ú7‡'‡35v;ö饬ï•^ñ>™†–ÉÙmLÇû3© ¢^Ò—Úß±Ô&ìLÎÐó}Æ-n¨xbm½ÙE¦ÁŸ8WêὈ:ሙOÔ*݄Ѧ2[•Ký‰Š_f(«[ÜÀ°ŒÑÊ’ýñ¥+H¼6¶¤ê8~€¦§˜ê /Y"}9£J¨¤qÜWÓ±GôØa£©gC/ÃèXAÃí`‡Ú×ÔtMþ)£ä¾Ì¢?{Ç%3¾ù·'¹àEÓrýЮ7È´­ÅÖ¥Yüœ‹OÙ8:Ïéå‘ÒÁ_XîB$¦†ØÀk½˜Ëå‘;Œà“H1—d̤š§8viOïx3À¢à”VÚ"ù m8Â>'tyg´é:Ù¶„sì¹.Rê‹[li!„‚4D,&Ó®Žê//Êø˜Óž>ø£Í%OïÙCÄ·1€a‰_¤ýÍ­Þ2ëàkzUš °–lhÔâáWBIàƒmñþ*ŠBT$æ1æÁyƒÓËtÕ1ÒÂj$Ô¥·9ðI~øOLìè z{¥'>Œ³-ÂÄÃí{‡ÛMAõ#€Mþ)£ä¾Ê¹h‘G·ãèý&˜5Q,ba+!ÛŠ;RÙ"áÈëÛ”Dh¹$ê~(x9Çs"ÖŽEÔ÷G5¨¸OQ{ ¯,- x4½c&)ÑBB‚`À‚…¾¤=Zð“™ô(>Ÿ1Âon!„x«§á´‰ÞôH"è~ΣµF—ûðŽW›–')Þ•$Hˆa 6â?àƒmñ¨ÍéíȬá4ù(ô½nb“INfhìÄ’r±3œŸ:®Á“s92Ì`Giul·îDLƒ¹ûIk§’»vóš#w;íÇáѤ~<Çá¶À"ý©£ä¾Ê·ˆ`>1áí`ÒŒ'Ûȶí?á|ý0RFa‰„—ö¶ô\røSïÉ(êÿ{Úˆ"?-ÚÞ±·£º²}F§ãA&y ñ$KäÙ;÷jxo/QYkÄ´-L©¡3–-¡ìú/ ؼö7ëKà¤{3±ˆ¡¥>\VÎæ© iim.V/Ë/p ¤®pÇŒ 1&P¼:'î”­ÏÀ”UÔ)ÿ|!¼>ˈ!û8ª]ûjO*Þ`.ƒ­Ì?6ú±gèññqMi† ˆÄ©C).îò{Š_A\L0±~^²›Ðxcì­ 3"ßuøñŒ§Ïˆ|ô®„+=<ŸƒNžÇ¯¡úìß:à<¶Ý±„è§Èþxq÷Ö=j<òÝ09ùÓâÉ:µ*ªßâö+d~ƒX$*š<¶ˆÏtÑCW÷Ü€wûþ¢@žëÓUj¥î»®%¯;«ÓŒ‡4œq`]žS2ù’Ç}éœÓpÊàK’º¤³Iö¸]ì˜B«Œé5U¹ÂÉèUö§[·² W.³ØO±X©§œ²¤ZFXUTÄáŒ%V}¤é]íQœã)hìW¥-°¶ùz#?@ Y©êߥáe¾Ý.Ÿ$pà1M0 @Ñ\šê øËË(A¢‚{8 ÿ4Íô¬OÙÍ“£‘0yƒ5'Ѓ~ÂrW­·‰Bæ­MªfÞWY Ú9¡¾JJH¤GÊGæC¡7ç@/'ÅLÛÉbC‹ãúüü)à’ªRÚ"÷GÛ\äV«T¥8Øœ0s–h¯!yuöJøÌ7í¯€ Šw”ÅòØp|ñ4m–J+ZM:]¸Tšý¨MÄ®¿{£ ¡Ó^ÿbÂÃÕ¼^lù*õbÙühX Næ|ú!¢B;uj6" Á„âJªö1¤*÷XÓtÎẏXÆß1”ñyAE¬å03õcG¿ 1-ëLÉý `*Mó’¤*£Øî qx6ª(÷%=½š®y.?Y./`ªyäCCbQ¶! ÛA~iNŸÞ;•ýÒì‚ ­¹®[-ÂóUžZ€Ál·]a\­ÏU<ƒ¾MÌXØ>þÐ ar7‘`_”Wf„Y×ü„Pí™î=’¨Ôc:ðÂÒ#@½¾ôg~`gÖª®þ²¨*÷YØÇpmüD4ìi#š‰ jî®™=–!uŒøUÀ-ó‘µ™¤WIp…wcZ™Gc ûËõqED­t[‰u¹H”‡obÓM#ô:ÍÀÏú ¨–øº7u€o0yvÎ¥.uÜ]/Õ§9|Ç•)üGkÖ»÷™”O´÷¸g*´ù@ )ÚE¾æ¡HTJÊæÓÂûÒ˜á9E+€ ËiÈÌŒ<m¶ÜUB?B¸)iéKåíî‚õ‘λû)Åù’¸ÂÒØ™UMÒEè™´Læ^Ú)(ÃÿT(tµ ²‡{£³tÎ)úº=ûüÕ1+t.¾;I:±Z„ŠòÁZº5y’ËøÒÜî†1 Z=17ؤ`‚“ƒX ŠÞÜpk>fÕœ¯ä<ݺŒa´ȳ)–ëë5'ó³ÝÜ +ä%ʪ×"ò¶Øl¢¡á³ÎÌÂ’Úë@Ò•£ð9ÿʘ놟â¦&,¯, Ià€WX:]± ”é2t'°Bl Jßùà߯_•zðÀ“\ü3 ßTó_kÓÔØöÏÝU¾¨˜ömt£wÈžÆnÙþ“™cÕwÃp[øFüÊMà8”VÛA}:[ e­c‘qr|­WÝAq·ý‰ö‹ä?à\(Âã•î_,!j‚ó0"”°|þ8âä.^û¿Íôˆ6Íÿ]·Ñ³#Ú?§én¢‘á°c(q}™±ï†ØE»=þî@¸1¯ž¿ÍˆI²°z}Ó=­F 3\—ÊO-4 oþÅóc›½`o€šÒ Ý£gÈø³°T‹!I#n§é6W¹òÿé„5Á¿©„T&›©Ü>Öõèþ*ë´-çÿJÓ¼¹ Á‹I›Ê¡\÷yÔFû¯ö@=v­òƒ‘Daé†aÞK;²øŠ¤.ÄI[šä6 6AÐ<›‡ÿ)¯´’–]NoMD"8›doá3 :µ7ƺr‹S‡ :_¢§ó&_³öÀ’)Ö nà"³1}áú˜?RÇêÖŸÕ¨¸ý´}õRWÕ©·Õ­¸/…ó­ýZ?ú´P¦¹¬¥›Rdwâq†Œ?Uë±KŽaæ{ ! å,¥.v²@¹ètÈš‘ÛZy6L‹Ì·ÞÔ‹Ålü·öµÍ5²AfÖ\bM²¢Û"7…“¡#:Údžë÷2ÐúNÉ0´?Æ™·PÈ)Ô$»šM^³_ñ¨gëa‚¿K}ßûUXXT»-KW|›ˆuŸîìyÆ¡”¡Œ€,“о™7“'‹¨41|ÞÖ6ð9Ü`}|áâb·¯úe¨’\«J¥©ûº'Q1Äʶ“ÙGEŒ®ÒítÇᯂM?uqA1Âz¨À4o{v‡ÏT››ce,ïNx÷T­° û|ÅØ1ø;H´*àîÓ¯Yž”Öd( J®m¥#@OÖˆÒ‰tw<‚i#¥nX‘;33ÌûW¿u…ú2}ðOÇu m!ïݦžb¦ÎI’ô­¢ÑYæ·ü­\d'¼®Çnš¾Ä}îÎ=èß§ç°¶”s%ZÏ Û©@ãC¹+²×ÐL¡øV`ᥠÁÏtpä3/;¨Ú®síʈº6nLk‘ÑÁK”I³{M®pª?úÇ­Žvê}3ž0l5Äšy¯šæþ32Zž =àèȦú`˃ ô¼£ä…·} -'g =Œ çž!ÉÀWܬ¯4ŽÞyù†É´‡-"_ÆŠÙïÛ¦§bÿ~Rs•/Ýö&TÑÆ¨CÞOújÏËæ¼Uîác¢ ÜhMÞ—å_§ !»8<YlÈžc–;ÄØB[¯úÛpèå¾6ŠÓ#DMY#h°;©Ñ5°…ø¶X8¢~æ¼7nßô?¼zfÑñÆÓíÏ-AI¥Á× v0ép‘’45ؤ ³›>/ɯÚû–/K~²d¦J™¤ƒda’ %>‹ð®—sv3Õ‰zÁ{Žò^ŸØß|3í 1QXT Ý#Ldgdš/Ëèm@ $Œ<çDïül lË„ ŸÇ,h9­*kàEMG‚½­ËEzðp´U" «ŒÝ4uq„{öcŸâŠ5L·q°H–¶D ”Еv«% Õd¶ã /Tw# •Ó‚­‹rKÔç°GÉ1q€Hâr1O+E€ü.pZóU]”]¡ÜƒXwÁ(;‚èaGÈ/­s~ Ù’rsÝeYÕd3 @Á?}±ÆÚÅ(˜ 6XѾVÅAÏÅ?€§q½C€Ã%@Üj Ùü~ÚëXÔñÔŽv.˜¹JØ.C½’öGɺ#Rì¥è¦ù=Uc ¦Â?­‘_=/¥ß–'‘^ˆ;?ãMó¥s¬°Dü„]r U¦áåš>L#â}Ä»Úwl^ë4Û3ÏœÆ@%ç$deG¬i‹ßÁÎŒ’¤:L8׳ñCinàEëÉߺhKÂê€ØN ˆýcE Í_Qàh¡µ­ÓŸî$±M#šÇ¿joÀÂb!ÃÚØ?aD¥OàŠ=};žôð¡_ J†ÀUÌÚ{‡øÒUØJ%ÊÚŸ(C™œSš¾C{([Á|÷þ3äF îñ€SåRѦ~¹¦#×EÍ·â % OE–еýËŒ¶Ò6Ú—GùxEÙ 8ž»lIm|XÇñ–ÉJ–s!†\®CXšnû¹6“QÜ‚ç×äÏ(9äo¨—±”ﯯÖC9å®Ug}¬æºl6¯Fÿ%-ƒ‰MƒY‘ç»ç‰œÊ§ y¤GrŠ0˜­¯m¯Ô©Û¢¥dU¿gS ædB’·5ÌjÿpÂàa:&H'8üÍ=ÐÝ1R©> B½‡n7 ˆAâ™$eÓ`8ü‚š5IÍñ×™Vÿ{2,¾³HÞѦ×ÿyyž#::[€I¡fº·hâîjhÂY«¶cv'µ÷&høçìñ%›‡bi÷·qØ08.®²áú“¿RtâaúœSÜ:4:>çSùÔÂ(F¦Ž4X ZOEûw¿<˜a*¢Ç¦â“»Õ™V®Ò·6åse$ˆŽczV¥9ú/üE…3ò„Œšs°÷ &»®^.,,m‚{¾EG¾Îµ†ùY2ÄÒúNÉ0´?&mî¥øÐúíîõšÎþ a{ú1ë‚-=õ5-3ì§'(£ÃYéO[ñüvíÁkŸŒOßZ›ìôí‘+¦·ËÕ½'Œ5?‹n˜¨0åg—å»sÆÁ,ãô7›àÅ2˜#X¸ŽGbð=øÁ:Ö9ªU€ð b ¬c@Ã-÷Zõ<çaÙt…åà éÍ;ÍÒâêÛÄRmAÛq—ý^æ ܳK âƒÚcGÞºÀ?Þ÷§$=ÖMŽ7“-µÔ&O<èÿM²9)|‚o|f£Ø€º|YÞÈ—6åœ÷º_W³©ð¿ÒËÖ0æNÎ×Ý/m«¶D[Uû0¹š[àãÝ4LŒ‡‹Lxâúš?kd|çζ…ý´|ùÉOœ¶çQùÕÏ„[ä ¦Ž4X 9]8v!¤RK̾ ¢ë{IÀº¦ž³´\*bÚrtÍàÖ˜µ¿e2fM†°¨YŠÝ6˜eóëVØdßšç­ºë\QÕ-¿Oºzqµ|]ÛC–…(jtOÞ£ìõsÙÂ0øt뮦­èÝÐ6¨Ã§ýt"Àm«ž@•Úç=DŽÏÈ­³`8Š}‹XæËiO‘ëçýøRÖ»¹[SUŸfÔêÑ·pT2içŠ2cé‰91öÀ†u ìðxÇ7/3µŒ~‰=šf‹CÓ@­Pøvh*HÒyÜHt¨á;’í‹À`SÞiE»ÿF„sñ½=ðD™À2ÛÙ1zØ„wÊa Þæ}ÚåèÉPcZ8V0—*)o沜°c°cã³3l¡Á@‹2½ ¸vo„„NBǶ˜ù…Î7ň£ImOþqU»lóˆxÿk­øP@*Iðœ oŸTÇ[µ> TÐ4¢èŸŸcŠ€ #¾SÖ¸ º8YR«­½¾Â7¥Ah1 „Ý7ÛÛ{”F° •K%W¹$Œªu2>UÂõ–t+€`¹*%\/1ñ9qBA¥ öÁînz&“± 3rÛ€†X—ÄÙL`oݵӻ_ii®Í}\Ã$Ðø$:ÝxøޱµïäbŠP&$„x“}´êÍ1ø§$=ÖMŽçx4ž7¿¿|>Lÿ>ÜúÔtÈ<ÊtÜ}â>|Â-RtOåœî˜3;}ƒŒªÇLL/$s^¢‘ÅŒ%>ók˜q^©Øz\h÷Ê=ŽPŒ\ú“â‹wy/Ôãö´þ×ßÛ¶ß·d²ûi[íª/·d†Øa}FWÔgûuöêh¦¹¬ªëõlÕöÒ·éÛé½X0SÚRãh0akè_CŠ]e;95î‰[Ü`´èvíÏ 0ÌmìÞsê rDfÖC8H«Rñ-¼§¦A{È7IÔ3Ý$³¦—KôÖIA‘N£öNdt2Zå9ä rX³”(4©ƒH÷MÿndÆì l YÓ÷ú'|UÊT¥É9Xu@‚&”N¢ ÃÌÅ8`ZuAÈI[Êwì8SÅ¥º„å%÷"¶8ˆÑ‚ñ7)ÛyÍØÉJ囹z÷«{iÝžuãYíŸài¸é8`œðïÉϧ½'Ú5ö=uÞCÏFê#›ýª‰Ù uj±÷_ƒøe(¼èT#.W/3æ5íÞ‡±‡Ð#‡-êß$9b±? •÷vÌú“AEŠÎF5êÌe¢õÓJÜ}5=‰+¾.»"/p‡ æ:‘Þ£±}¹âÓ»–;áÅrÆôåê¿Ö„¬'õ^Ll çˆ|ÀЉ¸á¼›å-ßw- #aņ»ö¯e0’)wŸ”sü"Ö0%)F>ð¶kžãT^ù0bGú,’9¢„ë$úd1(ƒ¾äªöIp³ô«_LYë(;ÕºÀY«ªuJÆ“w¼n¬Y«Gï'Gðl®qª¥¸¾@á{Ä£÷zÒ”‰íÙË_Ž4 ƒÜdöi¶ÀÅ+ØYjäVÊû¶ð-ÆÈ¡\¶È¬¡ à %;D¦„ÍÑ¥j«ÿRD>$YçýgÒýN°Oùø*‰^d‰¬²Óìw %…ùî¡¿÷ÑoJ¬%‹¤½úÌ•HÆ#ÌÒ6Y5 ·¸ê 3ü—w+5P±ÛÑ´|cȤØÂaë_Ì{¦ý’ ñ",PŒ$‘¨¡ƒ^EóÚ§›/Çþýâ«lpˆ)èO꾓ï/©Bê›ÊGSæ .O…ðØÕ].pßœ ”™zV®ŽæË}2Hø{2  ðeí£èlmt(ŽÃ[ÂLÙ#aE/ÆPƒXÞ¹æMÞg²ô Ulºj¸“#g28Í ãÝ÷°•}1!y·Ê˜˜ìí“qú6ß ¶½v œ’ŸÕ; ó½VOOñ¦Ö> PÓ)cGi&?™‘…Ãù²ÒkDßÏvPÐZ¢_&ÅÝYÛ@Íݵ ºßVþ‰2Å€J}ò0-zöî[µRA7 7á‚}®ô`ç_\}ØëD•’ÓKBâºÌ^-uJXjImâŒé¯ÊwÒß—–^ô„Þ }ViŠ~ÿy—(nwUVvɸ­ç%îÙ/œ.’Êé‚ ï#Ü"4SzR>¹Èì½9‚“œs@øˆäPñBðÝUŽ¡/Ä‹yÿUIB9±/9!ú–Ãê,±aº‡®ì.¬ìRUó@­À$ôÓ‚q¸§'EÛ¡X¨äç—å»jSæø_§‡vz]‹¡5›¥ýmÜ^ê¸w-ZÙ¿ùÄ´Â+EK}ÚvÒHÞt`êa³Z?E8H:ÓU ªß5¬é“ ¥°Twë(8AÛ'a3Âgeù%gôd¥B;üpƒ©¡ Ôí¨GœŸ~iÅ×ôȨJªÞX¿Ñ#±EÏÁÿHo«Æ—iŒ<çDïül˜ƒbˆV9‡)û¢§?¤/àlèß.!Ö‡û"_î¹F&Mú?Thˆ‹ 3ÈîÈHÛíðvÃY@¨‰¶“í2û9lšã‡à*O'(ö#›}ÊhüvíU?R®þ?@¤Œ%°FJË®Güþ„(ÄÞÇò3÷"Ò>’ü)¿b3S %_íùÞTôxþrl˜ÿ;+ç´Tš7cÿN-€‚ S°YÈ/­rxü+4åì¤)ABk8úÄscwŸ#ÞÚÈ#|áò_,ââ R6•±Q[HŒÒ©7;ÿxáÞÒ“ì‹Söh@ühÿz>Ò¿„¸ôí4¡zÀ$jôì­N;ÒËÃÜFpâ]\±m(ü†,¥¶~ý Yئ;&nXx‰ÃÐk·Éîk›ùÙO#%X N=67ÿ0#Ñ÷Ijf©w&†·I w¼Ë۩Úaµò¨¼“½Ö¨0 X}`P>ñ®ìüå-ÅÚ§Ž. þNMüÖšñÑ'ЇHCŸƒá‡^ã¾£ ¥·=qÆŠ€Ä Jx÷ãþ=–¿Ù¹¢/NŠ’ÚÌ{ Ñ˜¾,iUNDaEy{I>¡žZL"üœŒ$×ç£èOc;9 ºi1fÞã_嵂Úi⎶àÜ`½‚á»+`»²,üå¢÷׫Y[…)Ì™f€Y\¥i…(. ¤j”`ìœÈ](ÈŸI œ6àsj ÐöŠÃzþ—‰ªO~cD¿B1ILŸãÀ—¨“l– P')ý™:áí{«ý§ÄlÿiZ„Êëy-a¦.äktÞn¾B­À{Cg&²e×êÇ…$8i`¬ŸV¢{$MÄE”hax«¤³VÂ~ ËnšY˜rÿsXüÎæ1!?QÕ„kÐtO–tÉ䔤œ}ŒÛxj©è¡_«ì3¯B±æ›Á à'_H ¡èhkjÆ_ÎŒY œÖa›ä0o^â)#¸ :íÑÉ0k¯·×éϹ®:'«tôwö¸¬ N+ýõAd»©ë®ly"ù±&OŒ 4pÝœtoGMêãŠþè8`|(»ð*¥st`ª¾¬Ƈxz°ËÛ‡è‹Ö…áèQ*ÜCÿ/|ØëîßÕdìñÿ‚¯8#õ¾nêƒíЬ±å3åüóÝÀÀ¼éƒ³¬’ÖãùV†×è½ü­èår‚Ï-fÐæ¿AÁ]RiBßlŒ [o¡­7”¦ù=VMŽYVŽ}fzè1Hv>)µØ±#ùö~[t½ ž‰Ç6¥fÉ£¦l™áÜ-Hœ&åšC‘CÍðgÊÖ&‹ H½¥ß'=·YTgõ þ¯IÞd0H+l™Ý1EÕñp¿±g z¥W׳ñ J©uð¿Ù¼õ9M0ªô!n_X¥¸Ññ‹"c¶â¸¦ÈyðM“Å¿=ª7‡ïKàçá]åaX$ÍL Ù¼÷¾ Š­®ŸzýÏé ã¦u52mîjö¸æUÙy WËu›bWRsšŽþÀq@@Òg­ì°“L¹Á3t°ŽoýP«E\ldZõSE½«Ÿ áIZmO:êH\¤MfE~[‡Š+Ö5+õ…ø¤AçˆXYYpÅÙð×ìØ©ºv-⦂!ø±¼¾]eËçÀåÔ+Í19$é¤6 +.*„Ün£I—¦…«=l­ð‚ ªµ€—û¶ÔE Š9ùà}öøö]oQºec+Bõd¦j p÷!°Ð"Je…Âcþ_€/¯õäÁ¯—`³þî v?J‘Å5¿ÝL°=8´¢G^À^ÝhTZó´ï(¤n]qïfó”)äŠ(€ Ú O´a5 •nõt¯,^#9Ž.p‘œxîJQ2˜TŸÖ]³¢mà(pðA­9å$G2_±Bé«ùGLûcÖ#“þÎ,J PäGÅš¿Z‚Üÿ\52ùQ:£qÚÉ_ yï‘@BürÍöøÞ{(cù¹e°Ö ³!nÌyºµt’ùLÓçø²ƒG1™§bºóòV¶_ÃÎ@èt‘ÇÁKL 09ýÏÂ͵¥µ5JÖ9rÕbÈÂZÙéûÈÐÝî(¤ð¨äaÖÊ?Íìî~)*·ß}^{¸ÅKNS©‹–%Ãí‡ÛEAö¿)'™•A•hæz?›0÷­¶T "'bÀ¨Y˜ºÂ¢¥+÷iª\ó\¬?vÜ*)é>”sm?U´ÏÎãi õ°»x=öt™É1ŠÙüK»ÞIÅÄmª@WsðsÜC&NÜ0<ð+ˆFÇ«3ÔÍ:¤à!/µÛÌKg½-•™_ZÆJþPš<’0äݯimÁGÂD×Ú‹¿“ p¤AƒK Bq”¾G6f¼pp8` õܖÇUA÷õÿXÊo(ì‘ Ä› ¥ª‹&>ó¼§Ÿv½¥é;Î’n¼ˆfhÍn©¯‚ª^EŠ[E¥Ø9†çV··ŸHnèùºéßYùb”‘u9g!¤V32ª¾ e‡n].µrÉ4 © §TOét? ô@Œ-à£C4QÍE ;½oÉt~½€ŸÆjmS Й}Ñþ°4‘vÁ] „r¸—WáÊìÃdš¯o+‚$p9z5š´q p'Ñ8š¶uÇN°ßé,ZÆÃí2‡Úwê,)ä D·çmÕMʬ¥ÄK¥V®a2‘¹ÍäÏQÐOªo6bè Þø. »€•WZ,é|Ê’ºä—AŽèŽKˆBI¶_t„Vh—¢…z¬Â.C“ƒ%N!RÓvûvË´LŸì'Ðü¡Ù¦•»š@{R ZkÏ#Ë <;—ÄÙ9¡]ŸØ}\Q\f b©ù iÃí>‡Ú£ê6)ä DS>ÇÀB}4m£w@XæM_b'öçS_Bìƒk¨øöv¨qÓ,{tþ¨CT’Ô±¶Ú ‰_s÷ò,é|³º ¨š¢f=lÙà,|ØTÆ Å=c}p£ÜAä© FÅ÷âôÞ¯‡ 1FÀ¯÷º–j}+— Ìã›+s>pfµ”iGNv 'Ðü¡Ù¥*ËV?„YÌ|ÑoÝe<^—.ŸG‹t7È…â‡D‹³b¹TBñ2 3ÊêC¯ÇáIÏά‡áJ€)'™”ì)˜9/EaÌv¤^B¬µQøëTd·ª i»^Á«ã\ºÆ‰·cÿvè™Í'd‘`Ö$¬g øw|Ž´•Lu¸›z ø/"\Äè[^ g^ >äŠÕ´îÐz«uºÜ7¯¬%+/†ºß¨/‡ÌFº¬óÓ}œg>ä’4šè#le‡Š.{¹èO"Skê6ç²øár£ú%J-Z™ÛáF‹Æ„çy/“Eª7„ÀXu>B½c ‚>Óˆe`ÄøÕK¾O±È½ÊÀ¾E%(½ÜËœhè‰ø5+1å:#I‘²aˆçÈxÎð ìs.§4(0æÐ“¡e8ÇËÆ@ÐÓí9N,>l1ìÓ:¼+[÷)¡Ûdäøü³ Ãhéj ú:ÖŽ@bÕ/¹¢q*ug–%"•7C{¿ áKj‘îŒ}¼ÆöBØ%<ì¸e€!j(u¸¦ë”G”È{Ãr™1ÊO`‘mfËî¯ÁÌó]ÃíÐè}ºmÛr€Y-„Ÿ èï¼`ÇQô ;þFfüçì6è kŸs†YcI¬Ä1+g”þ œ‡iŠê73Ø@ÍÂQbÎ7¯ÍN±Èܵª¶°ÚFñã@N;]X±@«øÕj¶ô,§ðí5„t‘¶a÷ #é½0\Ùæôrýôø•¸¦ø¡åZËS5öGjIh#Øó¨&¼`ñ™›˜'%-3¼Ÿ¤Â¬”+~ë´¯ŽFU¢;’¦¥xþ!ÙéÒo‹Jª"ë‡\5÷DNSšHéÆƒ§]ûÁã?#)–F1Ü£¾$v¥Œ7ˆý²Õ0i=ÅçKcz††7©­v HÏÅŒ—”a¤¸P¤øm‰¤‘nà×çËk²%ñË_êO‡”JÒ½âÍeq”q²Ùo¤}§)eðg«eB€B2n? ¶8óÏ»w`òAý2Ú^ùÊtx¯4Aé¹,•-(d7¿æƒ¥9SÑX‹eYsÓ}/b*&£º¸ÎLÈX³´]…XNßlK$IæVÜ kÜD&6>é6{]Ø´ 7âLºTN¢Ô.˜ˆ[«ãHkÕÍœ!9,ãŸe5¥)Äåëõ`d5~…UÅÖ}"Ö±—'QbÒ¥Y”R„Èú]>ù‰©.Ëo´þ®H-Ô5ØøÊõ rG›æ·Î‚Î-ªmÖcÖU¿L’Ái÷À5¥<ÿß!aArÀÁ«>×p¢‹4×¹ àsèßgÖPÐp,‘1XèÈ/èð†®2CGZõ€ŠVJñNoÆ®0ó×T¾S_ºM_6 ¯÷vê¦ÙØÌÛ”³/bYÚd¨{Wƒܹ‹¤%@êèîŸÙÊ|´ŠÕ(`aƆcuaø7èmÏ‹lkÓÁ0,9AI%€· ÃEᣈ4L"žæ½|e­»…ìw•ÐAH\Ï(üÙàh¯ºUýýLÊB)ž¸õG€•gs•z'?Úk%´Xn±4¸ÿ+2JG4źãD2…ÄŸ—‘Éõ<¼î0±P©6r\"«¡_ÞÖéFÔoœÆBQ9!®Ö –á Ä–NËâˆÐ!EÀ((T‰¡ÃíN‡ÚÏêM‡”Lf× X[ÄÌ4TfÊ,“ö {’¶6ZxtgZ%hØñŒí<1}3«ËÌ‚üF(¥¥½xÑt­áÛÔOb8ÀŒq\>k`•Ö PpìϓTA8צ,Xͥߟ#–èZ,ÖÈÎW~Ëudãà¦=· Ù™À+`’άª—A±hLxMÐ'í}Ã…Á !HSÌ< ïpfÓlÃí[‡ÛB@ú‘X„ ¢ŽnòêÊXžl+-ΨȗvG%IG¹ÇåœD¾uíG‚lÅ–¼2³Ò{¡[U¨>3/Å\Ÿ 1‰nÌ ²ÅÍ aëò~Ü# 5Ž)ʼ°œ ]àJ×'P_>M‡”Lf¾‹¡?_-š#úÝ«;Ä3:PÈjbº< ž×¤|Ø/çavçÕh§•çg"Ž•P7€§Nóê¦IëˆiHC;‘Ä㎦tA Óo‘í¤4÷Àµ×¨‚˜Î+w¨.ôíúv &[ŽBZ|&ÌÌR™; Z©_i½°h&6ܘÝ@EÂÑ\ˆlsømÀršƒ .ä•—kæ)‹¿VuK@LªÈ¿ ÏÞ×Ë€¨Øã¦†ünÔ»Çá©3óØXüôdY0¢ŽnòëGJ%±9l©®]àyvÇ€')É¥O³]ϺHÄÅ '¼ÀÅ÷/BAn‰M ƒpÚ °±i<ù+¬ð£›²„ ܾ6 nþFØ_L—›š4Ìâø ù²aüÔcFÅ¢ÏéäJç uëñ¦ºjŸ*’´}ËebD;!iaËbíߓ޵Ð02Z&H© ¿ˆa·ÕDè.g;ž*óÌL¥ªÃ9ÚÈ«ÎmI8Cpkj-R¹›\?œšÍîy©ëP|‡°Ø~ð ×ÙGêÀw–ÉGl¯™Ýós¿Ëщë +ÐxCî˲äŸïÓ<ÑáûÛÜ„åpÈôðÝKk’š@‹¼=mŠ`²iÔ# ÔÃÑjŒëY²å)ÀÍõ-Ñp^&æ€fV_£¥î-*ÅwŸ£{ÎS– 1âd-¿KÌd>]b¬-ÿ@úkŒ„‘2ÂNª›í°z(× âdaqCõùh%}1J´€ø×¤}9Ü®ýÁ¤ç¶ÓWâék5fƒ-6™ àx"ïYá'“A£6H´àÖ ¤÷õÛq,ñßTM¾áyꆂ- Áx4E#ë?÷$ùOï¬ ‹öü+ö¨Nëôû ‚|raþP…£i-«×öÕé‰Ý÷¡EóÒ¡pÖ$oÏÊfƒˆ…Û©Šú õzÏ·ïº>“ÿn¯øþ+kz¨uû¥˜Š… ŠÇ³) ÑÔÉ-.CD‹òDýcýÇ«@ŽÎ¬^w=w? Ó÷8A%'3Q;4¯údã–g"Jª³ÍÈ[Ñ´D£‘°Õ(4÷+f·"¹ÀùýGþkÉ —||µR=â#Ûkt‰µ—4ÕÁ¯¤—Å«ÛØ0”¸Dϳ2To¢‹«Ñr[º¿ˆ…•ÀêJ³f¹V¡^ÝBUYè0jg¾QðnŸ?Áo§ë–ç"Mþ×JÚŸæA›%E"³Ä÷Md¹,K"Ä+õb¶>"a€`ÝÏkîE˜%«GÐ$,Ö–ªP³™J‘;nþ¯?žÔwÃíÞ}½AöëØ"ý©¤'/ò#uølªÑò®ÏÏ4k‡•qü§bCÎÐý÷=®‡ÂX4îî]ÚÞ¬R’qãaX"Å¢H-uŸß-¾²ãfËâ)ï©fP" h/%?ÎÞ}­}i?fÈç_J§]\uÏÝÄáÔqRæa4#k÷Õým¡NµÿN "Õ7„$K(ü–_‚l2*Vn——ŒJ}úT,×ÈÍ'Æ¿÷nÕŽ‘˪ÃZ57¯ \f® <ú8á{¡îGË&Þ¼[¾ýÑ4ÇÍ—ÉB°Šg2Ô´ Ðb™RbX ‡îsj Õ&—¦lΤ`­õ4Õ‘ÁÆyj³=íŽÈ×u¡Ê‹ƒ b+¡JX#‘ïР¢e“S…ñûd~ƒƒÈ城µ¬‹Jï‘çÿyé\®,úÄ ì&6ê÷Alls/ï™zHàKmwº– ¢_¥W§¶^®ù{Œçúz £GÇëP¼^8}zK­éåHjÆQæõñÐþ!z@ç5àî „†oöHÑÀ£§ §ÙjF¦%Öpž-ÈÁñ~O¾)ý•`ZñŠëŒÂ€\âðXöút~U§=Fy8‘4O8·Þ£­’µW#ÅÆ-äëËö™–ÃU@ý )•X¬>:†oEà’ªRÚ"øgfw .(K«FóÐÞÊ)ßÈ×{sÊ5KSb¬N`ÕZ)Ç@é£ C%y_¬Ï@Ö³Žù‚9¹þ.û©Ë+ûR‚ópƒ1çihñb£úþð¨i¯èc·# 2£Ä.Hÿpvô;à ™Yê-Œ\>hl“BÙ(u¸’”Õ’Î’©N7Vr Ñúqžˆ•;ªš a%Ÿƒ{?^É¿µVFâ6Ú³Ô;Œ¬_R |\cè9pCÏz%@A0±¸é$ÀáZQr{Xº¢¼ÂOÔ¢ŸÑ‰–1Íq92› ‹´d’$µ±Ë7òØ„ÄÎÀ° joyd†ÚØß¡e1WFmÎT=VÓ#ÌS‚‚1”ã£Õ €’Ϙ ÅçVêQb¢<ÚëV7‡±Ý*g3e/ ÅkN‘|¦ŽïâÀMÏfdbE«urý×ÍI<²è·k]^WÁ½NÄMöÔL8E“=¾bÁ2©ø†Äevó‹'£Œ£ÃgŽz+ÌÒÖ~Ÿ´;2ƒ&d„GQòcq¥ùÆ¢T_ý¶•ü [ŒÃëë{ÛßgJe‡2,ŸÜúïâE '¦Ú[BÛVÒM¸±%iÍbU»Êgö'BX̱yS”²jÒ”‘Zr|N—{2Mè¥öLiðG+¯cö§,v„'DKƒo~h_+ÿ#póäœÅ)!›]Ia =þ^ð¿ùR—+†óÍд)ÒE‘]ôPÞüÛ'wfþ¶Î¡ë± Õ–§óz{,’­Ž ½Ü׎Õ^ ß*›ëΘ14ŸyS\!!78èPÆ’$¨&ešóöŠÇþb [ ?¾ªÜhP¡¾„Ì8ÚVÀe½î þ· :Pd/+&‰,¹J4&ËŠdZØ ¢`ÏÃqu¤íöV¸(4bjãú¤$aO«LAàž£Æ¦¸M$ÓÉ¿™¸·rÎ.ó!éíN¶äæ„…ãæÐ<>}ÅÌnŽÚƒÀô£¶å(jíBh)ØS>ÏØoXÎÎeOï­V_ ë ¼ï­ÎÒÄÅ„jm7¨(4‚ãŸk ÈÉ2×å¤w’mÕç«?ÉŽgknÔëâbåÒpµkcàx¥S`Ë”®žUÇY0ds¾$ZèG³ûÓÅÜMM„~^U™}Ñô½Âã)G2žë0Ãíh‡Úñê:Mþ)£ä¾Ì¢?x¼]g¹©á„P`y¨éªÑ¤Yøª“å¾rÇ`÷#1—ø›1T¥‚ñÎ*ÎÚeMìÌ¿uDcÕÝ‘l¢Éi‹Ÿ)Iϰ…jûHg8AD¾âè(±&{K!¯k@LEà”VÚ"ù m9ÔÔÆúæ….®HG´T‡ñZëvèâõCì‘Æ”ÏÔrH«7 Lš°q§DÝ¢±ðâÌÙ®öô<¢#ø"÷4:ØÁ3!=UC®ÌÜ-ñÚ¸aÛþ dáto—ãìN A&3r/Ti:âêCEàƒmñþ*‰£ºl9²Jð½ô€wy‚Õ¨3ß ¥ ©E–E´:æ[×±pY%s$þæÓT±v¸se3¬Ãí‡ÛTÀú€Mþ)¤'/ò#õC¯»éGûe½ÿÚØQòPÒxÒþM/P|¡¥Ô“üy&„Î/ÇHñ]l¶tÖe ?’îçÖe¢aÐ}ËÔ¤Žþ÷ä¾¼ÞŠÝñuò_DHL¢Ãk‚ðÂfÖZÞ%£ŠÔÀ_jw÷'O ¾?íHCà”VÚ"÷I&¥!ÕNnt< 1Ú ­0P10˜+·i5çptdOônÐúXIõ·C´¬æ* ›.¶[àžÑÐïÑÛòýSÏA`Ë,W'ªÇPâí …‹$€Â9©i+6æx…;Öº ᆄ²}Z»%‘¥g‘°`ˆ€ú„qR“ Æ!¦Ë‹|sã˜<Wx•ý55\cÁ#)Ë™@i¤~àƒmñ¨Íéïq˜Ç}(Óiг¶Þ@ÿaxPèeTXl’ëÏÉŠ­y­gòq‡4Sê· ¿»¨üf½vaÎyÇÌàñÇáÒ üú’ÏsÀ"ý©¤'/ò#ìˆdè9%—ôÏ¥Œø°‚o'2EYI#+ Èn_ÝñÏH`qFù47'q©ªÂ`°Lk)…'®µUŒë®ë¾À/jZ¿)*R\hÔayÿaæÍfT0 ÷× HEú‹<âRz·+máz¸ÂÒ™~h½c`±¼ú§oZnHJ®Üül;Ó‡ð©$[rÖŒ ûgD" …äáçdnÈ•bÛ;O¥813þâÂ~öüáuôžK}d²+ôªRWðä¤ÄaÄnCè~ Zt›A1‹è¹˜w:¡6ñ]‚Hê/†5ê|Œ5ãB¬ì4û.'y´¡øµå^«ïEß 8Ü,ÿE‹Ôɛ“ÑT?7 žñ]›œœ ¯3|`ËÀïh- pÉäÇ=¢¸6Ík[Õ·ðsßHµìå÷ó†…™€É:4t^ÝfÕà~3÷-s‹f”À«Ù|0yöÝz{tÀ aÛQdÍÑ8g1?C2»ë> vkqòXúé´ƒäòCƒZä· §!`‹æ€¿&° ÕJbÿ{“BQØ °‡¶p/,iõ@v]I 5+?‰l5ö^>“ÜE$Ϫáhå„-ùjWáö”Ü3)Ü=«Þ±ì0ŸœŒGêà¬y1(x,fʆ-kîFèt ŒŸÿ*EìjüõDø/š>§¤0U¡|»&ÜŠE£¢G±Xæá<àpƒÜ‚Ö|ë)¶E«á5icˆÊÇhÒ‘ZòZ¤Ô׋ørÚ±§g5‹µ&‚g—ìö°–KLR¾&eµü¹&ȇà +ÓŒEDœIPMÚpÏÇ[À©^dMõ„® ûmñ*ÞöN|çHh-—¥~• UõÕŠð¦NT0“”¡yB=EBÊr[ìÙ¥ñAäJ Ù9è­ÂyÓÔs\™F##5“ Œ…ÿ­þZ€Âd†`º|¡ ÒüÌÐèVFH€¬ ˜gÙsh QäZ­ˆ©)3‘Yt¤¾É'zØ5j‘Ð"L×K‘+:Mcée¨(*϶ãCi3„yNÏäh°&z9XòGtÉ´ü?;Feíï†, åð±7Š­zª{…B ПätN½ìÚÙe/F¶ëߥT?›%,ÕÐ’WEñ tÓM^¬ˆê†zßOø¯o[÷䨜ˆÿ{€yØi¿?Ù•^ÍÎN„T渋Ny8 ¡n͹1¡äë¾’Ne4œI•îH™ûü;gôñ/G4 «YP–å¤%váñÄyÁò²³ÄhaѸȊYPQƒuW"ôŠ<¨"İÏF7I‚脊á;é˜Zêÿô¢Oô£×èe¾²uõžViƒÜ5.Ä gؘœ²ÔÂ(çx:$“[«¾y‹L­·1Ê1¡CêÔ”'Òn$y æÍ¿ K8CqÏW=¢Aý ©FYÇå0=×ÿ€ûçU¾ÖȇžþâÅ>ÈŽÚÕàÞûz[˜$–ÛsÇnàÇÙÒáþÔá+î&Œ”¢(€ [A_ýÄü pj¹(ô”’J‡ƒ=+|‚ ßû¬¼’(Eù­°Ì5Â@)Þ¤‘ÆÐÊ¢´šš>Pn›°‚–¾{ô369B²žžµËúÂEk§í Ê´—Ò˧¿j¨h-DAQ­ÇÛ@å–Ò[ZÆCOF}4=ÆGW‰r€`QùÍìó>Ú·îö|IHÃ˹K€òRÒ’‹Lášý›ÜÝ#‘7e_á±ÔÏ;ý»|`Ð¼Ü >YôíeÂ5\_¡¢­ ƬZˆÿC ƒÍóíº™K¯‹IˆDƒ­ZI–‚™Uc#Y³EYð†dÅÁVÂÖÜܽêÁÅO1¨Šß/BWÖÓd 2%ÚrHëxÌ{?„äÐ$2N¡<¸zm2;ÈèãÀ™ª'ÞØ6«A&üË¥Ù%S!‰0÷/~¼NŒõ[?˜ÊÕSÝB×›6xÄ0x ¡bà·HÌRlƒ2¸éZ'×@ìÉÔ\ŽßñuˆÉ¤´`¤;¡¿„ ñȰ§"΄×âЗ0NoBDc ÆP°óÓ°qléÅZ˜¼’„Ó/†ìÕí}‡wŸk¶¦²ó•dv"Ú?g’‚À‰ÈþÂ6¡0Î;Ô3ðø¢º„®?µ5ts‹"“Pc¼ïsT€u9ýùŒº¨'ð Û±ª$*™CЫ¼‹ê2¬¨³¾¡^‹¤vôV’9àáûV?j×íÖßÛ©Ø~×ÿm n«~ÝaÂú‰¯¨—öêíÒ€§…œ“Púwn þÀáeÙ(¸¯7n<· *@¿èY»¹à¿­ÐÁä¿mw%ÔÖúLJè+åÂÂíÂRÜYø8ðf„Ú‚á¯õ±·ÔR»¯›ÑqÛßüå¦3²*Öˆ|‡Þ¢õZˆº¯Ðßv€47è&¿ªIY†äÕ@ô°K€{Ÿ;L¬ñk%wÛû|ÅŸâû2®ñ]‘áÑ/ðaHF·´¤§yºæpéñÌKWQ T¯RÚWŒ`È«Ý~&áuˆ«4w^éòq¥Ûðè•õŽñ˜óüä)©ÙåÝ”Ñ:Šv£óq'——û²·'ݘ•#þd»»q6rûäíTÎT#ŸOÆÕKàÆØPЧíùÏØâ¢Çÿµ³­~­l¤Nas9xÞðJHXû€:N »ŸP¹Õ™g¯FèÒÃc™ÃØvõ¼òáèö—ŽjóªÍœïüsÒižÐǽ¤hêv _l"ZpíúôÂT÷‰N]2ãË”ëÃ64X£Î»n~‚.¯Å1þ`›ª0ô¸•X"^Hº«X "š—Ö öž'ßêK½„6)0ujwØŒÁ“p¿÷ì³ÝtÒªÙ;Õ-\,,Qù¾/Ü8ú ; xžs¡WýôAÿw¢¦)X¬bƒIßšñ'>ÌËh†Œ°øš\凂`\÷œù©*š…Ïì/¾«“ùˆ DϺù”Œ¸Ëü;íë;£¼ØB fÉ1Ó‚ŠsAàs¥/ŒZBE&Z°cd5ºKÑÚ[¶^°àB¶»2-?Ð|MýPK“Ô.ƒð’Aˆ\¸ì«‡ŒVŽ`oM½„æðß¾’ýãMNr59»cšÒ9É¿•–P¯îµ:ú6žB¥I¾ƒ>;¿.¹Ì¯áš`˜qçk¾6ä`ˆ*º(gnîtêqs+Ë;Q0M¯§Â¸™È*é­Š´Ÿõ³äŽ Ni­×X8Ê’Æý.ø'~K@‚l‘ ±•nR`æ<ånÁ¢²¶A#iB·a‰&;dsªOJÒQ÷bˆN‰ d®P—u¯¨²íCŒ Hg¾Å¾6›ÖD²Í©cµõ€mÔ¨à;Iˆø6 Ž%¦Öleµ7šÊ8½¢‰¿ÈšKˆÕv‰~×{ÔVâG¥qoÂþÀªä¨äç—à‡:Ù|´G×#ŽäÜÉfüà…Ê0V—$ýá ŸB6ÉÙ»W ™J@Áwýð„€Å>&™íñ–áGêÄz $èîÌHF iõJ=0‹H_KU-¿t«WÛù < €5€Ò‡øÀÐG¿« úÂŒ<çDïúËT`‚½XùI]p&¸F–zhÕß° "½jÏÑNöÓÂ?\¬^#c® Až‘cÂ!%òPª€aH¾|EgxÛaÌÏî¹æÁ¤ #ÈÐG M¤$5·¿o” B­'Fß…I¨¾¢B  ð2ÑF° Rh€0Ž‚’çÿ1 üâ X¦ÿ‡¦tYXku) Ù‚$Cí/{îßo±™kf.00nKÜ]ÌžEà˜×– Všs=ç2—9ܳ5v«<“jl=FÜ0B(~ìâ”|Þ‡ã ͤvvè`ÄðpKhÒ¦×&›U!ÄMMߥiÓë7—àIj__qKöH½VA§J…jEñý"dÀmC1ÕádÖdTZÀg´ˆ¡Äte¬èÈ–×j“JûÏ48®è«ò‡ÒÕš‰‡ÓΨiJKtGC‘n‚O‘ÀX1…κӶ÷ Mæ¥Í)}ÞéTÀÙ]p/²Œ!H€À?[Y݉SpÄ AM‡§Çœ–ÄwÀ¡Õah±«y'bÉð!y”LHPê¦Ø9AŠÇ–Ö1”’møˆÛ>ŒuOé¿É¥øÄø/¾ÕÃÚŽ‰G´dAÕˆˆõC9·Úá«„|xa;l¯¯Qö½äjCð›ÍKx® ¦’­uK¾tô£“¥%qÅ·S¬Dgà<Ž5³Ò߇W2”èÄèëÀ{ÆbÙ+ àÉâX9¨#UÅ )P28ãŒÔãÍŽ—_à =²Îo˜M‡mÇÍŸ‰Œëg££…ðul‰© Æ Üdí%c!«æ ›å A3˜Dzù¡è/þe]:.&†2çŒÎ'ψ÷ÀÀÞVoMûc/#ýɉQ%ñÀ¬Ð³›`bs÷Ìä>É'6#ßóL¬ÌyÇWgðÆÃ³>—$c {kÅþª·ÖÒJë~§°aú÷CASŠ‘wZÎ’›Ó ªSâÛú®gm lp ã3–ÒAàÝ °C‹[w'>º™÷×¶½9ÐÌžÍq]”"kâzJºÛQMTÂB6«ÅË¡?:ÎÆ…c—d60J´kWá»^³Ì¤¦2ç@¡vþ¦ù-‰ßC2rÚÃ#ÜÞhÐ-ߟ¼faÊå—»º”†˜Z!œ1Öïƒòµr‰C?`_¯Xu6ïc?¶ €dÉvŸº¸×¼ÃÑ׳ñ J¬ «!n,±([RËÉ´d³É´+n2¾žÀ8m*d RþÒÄæý½eüì!'þÁ6LE™+Üî´ìIä§ µÞöƒç˜ò¬„°Ì®ú¶`RÓqFYg‹¤B'—ÊϳGwL®Y!SéD ¨7¹Ë“›tMiçÖ5­\?ªÂi°¯iÛ F$W½ä§Î†1"ß¡ 7b½c^Ç#ÃÕ™ðc¾òñº}Ùo×õ™É‚w¨ûE櫾Åì@S÷—p[Œ¼oxÑË fÝí&˜o%£c€†ý6xŽòBŠ'cæ¸×"¦£x»;3í)ýfsë º©]ÞE) WyúM ”É)³~Ê|[F–<’Ã|¢*º4ã…X§š.ωcE¦ï jY@ü9Ù#çu2ö6-ŠnбÞÈ"—GðNhäŠ(€ Ú O´XžpêíE­…Ç€¥ï>¡$?â1 gŽA/@{·MòÂuz“?4Ä•i‰¢˜nèùÐÿô×ÖüœªÐí­GçjHÌ+9ib[Mw¶Mf­=_áÂ$—ïC¶54ü ”-Ö&pGªÕ8Öë¢?]›k#(‚¯™™c‰T5j¢ýiçáè•w¾ ©Ðæ(³\€_¥¤½Èr"H°oƒP÷Ì…´éƒ?1¨îÊ;SÒ¹HÞÙPɉŸÝiL|¯‚¬ 4ªÏ«\¥¿çÒ%bÈ‚o(›,UBê Þ,˜êʱ1Åë‚+…®Ó1MšÛP(±³–?/¹0­ÝËÆ€H:ûbŽË6mÚö©ó©ûœ QºøSúªú·9VÏÞ9°ZáûSj,àG‡íbý­0$.ÔKúˆ ˆ§W»—&\ŠlÏï‰yÅ ªrpÈÖ±o;µÁwÞŠ˜°íµT+rî-¿.ê%F°gÍù8A€üìbÖ¦ ùÍ3b6Ÿ;¨!+î¼4ý¾%å§‹¶FË—÷ |Ó°9f‚ü¨ÿ|DjKæÐ绹Älú=b™Z’µäÑ£”a&d“·$«>ú_~\hH@áè{;ý¦ŸŒS€‹n¨0åg—à‡:Ù|´G×ö8à "½ÕÎØ‚+i‚€ðI׉+éB;±ƒ»‘–‘󛙇Õ= 4¬O¹½¨s£›Š ï”ÂA:ü÷~ V¯í|ª+  `/€3r4ØGÊ—ºÀ?§$=ÖMŽ§Ž˜¤á6rÙ©‰!fGÝÈ„èö"™ë7åœü³_õÜù šGµ²Áô`í©ÂR.ÜùÞßÖM›ÀÊMrÄŒ‡áûW?j„ùÃOœ6¶ý´<|ƒ#äƒõþ¢˜øD„@§W»—&\ŠDeü¿­ÿ$%×1‡i‚/$¤¸ÄóNEFrRjKõËl.8mER%žQdÏzƒœ9èÑ6 ~¾UÆKË$ñ!ÑH Ü[vm‡Ù[Ìî¥+ºù»¸­<å§‹¶*Õµ„ý¤µ¢=æïWƒ`Ö0îî]¹ö¢{¬‘ –/UŸø› öi–gë|ÃŒª¸¥[=w"{ŒÌ{µÏ19êÔžÉ;ò­òfIŒ~Š ‹C …²ï¨0åg—à†ú£Mº¨oÙ[td/lœÂD•BàM\0ŠÄÈ6ãêD‡ÿZÚReÜ`ÏaÎ.â éŒ$‡Å¦˜f@R>³Þñ*’¸FH4dÄÎ_%@£zäÞÒµT$IɸjϦ5Sº…dWfÿdTóh£ ãs<ÊJÞyåœð.P’7̨>Ü`÷<‘‹Ÿ%Ö ‹&—ð áÄ‘©ˆh-üŽ€–!;Œ\þÈ‹w~œãø)?á¹Ãgqü*áQÿÒ ¶Åö•ßiWð×Ïᬧ…œ“PúvxßÏëR9­‘(mžÆãq^n·ZºÕª\§¿ò´ö+ø a™¤·—Çñ“<Ϥjλ†~­¾½}í²›²8×ëÍÓPR®DŸ žå¦3²6b×ÑÑAŒ+¯·eT¼ÿ„hB¿ÌoûAƒÝ÷,cn)ôž´ Ó2Ô%Y.¡¢å-Ç•=&7_-þ$S}ØÛÍïãÑ›ç–}®Šßg·Íí_ó[ÚRS¼ÞLùÌæÄé²<¼*%mì³¼ß>E(ÞnÉÐÀ^v:}ò/½·¢¥Y‘.û—]mÖ[ù)Ÿ}³5ÚÎÆ¸*²BËqù”ÞIÉ/ð¿ž&Œ{×@ÇFýûáÎ ÂCĹ˜Â4õôð^ù‚Ǧñ-þKôÒþýÕæe:ø:ÒÜ2UÓÁÒÎE-Æ~"ÄM¾F¢¦¶˜Ó^¤¡éÎôδá8GëHy-½xíÂu´rZË„+ûÚäý[‰b\£è­Ë¹xɖ׎§G~òw­*IÛ/„†ðâõÕÓÈK©1ûß'ÇÜ_ö¼Ääí&•ì52!d{&› 4]΄,p¥ðŒî/£9¬Ú6ÌWÔbôÍîÚC©ºi[Czr%¡ß9a‡¢u[2 ”£CE¸•ÐF¹?]Ôð. ›¤÷<ï¾×…QqȈãPâˆç/eF䊚ÑÊ™å;‰˜ÄMÀ˜?~?; z-vµ˜Údí¯ÚÇÛÕ!a|4€ˆò‘¿˜^ñœÃ¾ýäÿŸÒJèu‹«¥”Îù4ùí.&aª¿o—œä…Q&õÉ ÃO ŽuâÜ»Ñ5?—‚Úkpßw+î9ÆÆ¯Cl:[À'5ZMÖºþ0´V,KtÄͲmо¤G‘ Äy'–gÇ;ýÁ_¹²Èä@òr.ntà,ؼjû°/$%ÔøÀ˜UÏë¥Øã¦9‰p› QÞü‘faR¿d/"ϰfqÝÅ^ýÞg{Š(Ðùõ>¥ÁÌ-¶šÆ¹„¡kZ»Ö«ºÞÐdÙÍȽàLë£ô°µ`ð”,j”ƒ·¦IQÀjßÿ}êæ™k¿›–g®¯m¦âƒ«¦Ø~…ÖÔB°¥¥z•ÇÁÄt£×ÏkΟ˜Ä.B>½*ÔÉÿ`­;¹ÈݦÓ6r„RœnÏ=Ûƒ\Þ×È|bõínÝ¢k¤¨×\éó¨äç—à‡6<„oɨ=Gi¥'~›¾CeÂã¤4È+š/¡Ãÿ5 °yò°0ˆê˜fÖ—/õMa ´½ñAuÉÄ |Q®œßEy^ø ü_J6Þ—»d¨%UIÎ>J€PòòO§ºß’G€{òTºr‚€Pî·§ émÔú†/3¢²F*®Ð?’ Œ<çDïúËT_¶YBÝ]'€~ˆö‚|²Õ0ˆÖøcaÖ í>aaèd-•üPµëÛ ¤ÎùB¾¸J9—Ë·ty0ü:éÞ Ä¹Å³ñ)5€>÷c •Ñ‹ •uþC©ÅÖvãðf’?ç¿%@++ 2TÃöæ,hå7¹TŒY# •D td¨PÈ/‘`ç/²UJO˜cŒ::@D’ñ‚çٌۼnXåßì´ÎÖØæŒù·[§¥QvЋQ¢& J}àÙ¥Õ¾ ±éþ*é =uY|ÛÝÔZ Ï#òÑt­ËÑyg.3¿¸ÉQi9yÔ1ŠZ-… k0ÅÄV‚ÚƒK€¢ ˜$,.»ÄÈÑœ)·*íQ:…ê  R4/Ø8úsœ“Å•p™ŸÙgªs¿…dL:ät©W–’üd´rfè|‡ýwʼnz†—ÉàñRO¤£— ¸ˆ¿KèÕ ßŒX&˘Lµ X#g#õ µ±…ÿSCR‹V§ ™{N)hÎÖ´N/ /1E_œÄ šÇKÌšJQß›w¶‘ßNõŠ´:å–Å\DO ú~ÉÐVí=kXÂÃŽgXºï¤·(ø<« –…ó * …o-Ñ)”ê´žl¬›†Òæ7§ŠƒT´{q“QE‰×Q›n²ÿUÊš4)¸&V°I_&)¶n¦3ÜùE+ÑM—K+û8 #Ä, õäeDÍ÷èMD±®]ƒV\lbÓ'‹Wq›Ê¯áüùLo0_Òðœ9Ý6µk;ö§¥«ôÿK›X‡¥¡³HW‚}í‘N&)lFw>N¸«û`÷þÿ`e=~ S½Äè"€í4w±±}ÐQz¿|õô½ô±ÁT/xYûW“qfÿU›û¡ÛZ~¾œ†ÜHn=£@ƒ=B®¶Ó% üôRÁ˜„ ;»ÞÄé>Y‹(V<•¯ˆc’³þ+Òn=vl™îÈ–f¯5Q0G³j‡¤$¸køZé•k-QSo¬Xt€Nf‰P·$O’ÌÕök°ïGÔ¸®O´(Ѭ:ÁjgÀýPVuÀ§HÿGO’äË"˜^5Ö®5†t™;µîåY¶¿Y½»Ì…H<Þ(š¨ ÔáHUɇäGVä7Ϋ/¤Zzls¹–U©Âz£¦ù=VMŽ§Ž˜¤Õdœ‰©\´-‘a;>BˆåÐÚ=éŽpŠâ<#¶_ÙåšC‘CÍðgÊÑ$ŸH5ºux©~:c ïrÙ6†ý—Ýì±¥\¨Ö’׳ñ J¬ «oCÙv¨ü‹5‚ÆÎ=ý(i`^×s(a#9ª¤ÝôóÉ.úô€¬ #«üØàoÀûÊýª0FÌè„ä„·&“Ýï Þ‰¤ƒ#$óÊ~‚Ä[Ó¥¨ºQŽ›êš#IæÇ¯Í ñ#l}•ú#†ÀÄ–Á:6¦ç©h*ìAGvÁ‚Ø«ã§=Cib*[B DßK‘XcJ)åÌ ýÿ1oÝeªOjŒ?˜¡:‡lÁ‚p­†§ å ´¶#:ÛâênΦ+Í “sk›'_œ² DÙ ¼‚”JfÜVp » ØkýËh¶ ,ë› vаɬð dCÓKZAÕíâ*;S¤’m\¿çWi§ZÓØ±@@eøó°ŽAyë:BsE–•»ÕòE!ĹkÑù¾B®ºEâg‹~ù‰MOÃîÁhšž8Ruko<”ñýmëÙú¢Û‘½)W¥DFò%"k*…² Ög"û%ìž~á%¨˜ûg–]+ä›W‰V¬&=«K×2»cõÿ3µ ŠÌ›4ë2TÞ¥û’[›æŒô ÷úVz™µHê¡Ox}hge‹Ý‘áõKœf1³¥³_?xþ¼^›6Bâ‹ñ­¶¾â´Ñ­,‡œÌfYšþí¨i):ÐJfø›¯"ɹî´…WÜOÎä´Sqܹÿß¾‚Û®a.B_ûìÍ–m¦cz«ô1±­Ñ¶Õ€¹ßè^‘©AV<ïÖÏEùØ–`<îd²Ñù¸ÚãäœCŒÅž|Ó„ÃR}2OÙΜ©£6Fâ¤Åýw@6ë3)ÝΆó¶侩;5aa7 EÖ¶ùÒlÚp=æÖ·XÙ€#}ó‡òŠ1à ,ø·oá^ øšE=÷}Òøht¢»ŸðúP!Ùè̹ßèhv¦¹·ê6sÈÏêÂ}E–àá–Àš‘@èþÒ/ö‘Ÿ´›ö“?Úûoö×÷ÔûD¿mÙûokê Ñý¤ÿv•´³ö—8ùþÛ;ý¶õGÈöÞÿ[ 7Ô¡~ ½úƒÔú‚õ~Úsý¯Óç}@ÿ[\~Úê|áŸÑêÄ•b»»üšÿR¾ëjp¾vž‰ÆQ ;Û¾ÀÛá?RUüß4õuZã}·ôíç•C:_ý" Iµ£€G®Ÿš±Ï^ÓÃÕLÆ%v þ$ñO¤}â9aÓÚ†PCµ·žnioƒ<ìîêÙ¹a Úÿ ”?¤ _®h«R.­Æy@{„¼ÑÙ3hcááâlOÚI—dÑø•8×údñ¶Ý“iTbh]ÕWGZH…0ЍÒË C‡x¥B¦ÛF,ÊêGÉm•lsvÊ·‘•:PK%Kß#CªhB‰í‹ë6z xÔ&¹ÂfÐ[ÛÜœ©°S0xL”VQ‹ñÉgÊ ‚5Kê¯m`RÒÐ=Ty5gÞ*™HþÎOZ•o JmÅPÑñ]ˆNt„éãØ€q´ LüzyØ 6 2¦ÐÌÒṎ́ƆüCªÛLËþ˜—¨PææÆÎ…çŽ¿ž_šasÿzßÏÿÑíX9 š€2‘öÃßM6ã¶æSB¾™( °´éI‡]ETµ †ï¿¡(:ˆ×S[?àEFrÊ/… HBïE­QBpdIVøGޤìG ¤-òì §c_¶ÅŒÔé¿FL“8¢]ZC†™jD±Ð@v&‰ý½?à#Ó´ÙúÓ±pZ 9é~z4Áe×’Œ8ÚcMÚTõE¥`ô›ÜæCÒ´DÁ,%áÛ·ªlA—ÈÛ°Ñ(Ââ=3AÄ_fÖÚNϵõþôʾ,?ÈA•P"D·Ä^cyXÕ†ò•^*,qü,_GŸ%"´‡†ÿ÷"ú1„…óû¨×mu!þ´Vy "¨]/îñN!-Dñk|GºÉsø2û=oâ¥Ek2勃©suˆ‘] 1i‹á\¯qdb^ùÉAíî2 E{¤…›ÕÎÀˆU¬ xXEB¿¯Ÿ¿Š¶[«ã ”é-;twH\,1„Ä9=ðöª}Ç_õ'EEeêi=q„„¾,+ð‰œ-JúðÜ”2Œðð ú îBÒÏQP’N€å(UÂB_tæ³ [ã/iÃâŠÊ E´2øNÉàáZ,F€ŽúP曺¡Ý¨WöÛ`èCl`!ÒÈH¯™¿ ]bRÃÃÛA©ÅW%ÆïÝ´/à aÇçÛFrÖS-pLÕ]‚HÉO +§†*U+þ‘…K°n¯¢3‘ÚÆM /±¥„¨X …¼Î¥ß*ö˜‹3)²à]@äùãÞæü´ÒO_”OÓ.DJ?‚} ¯EyHq1äÛj¨ò7ë5{ÿ{âÑb£ŠÝ¯6eiÌõK>8]:úUßÅÍq›¥£—B`u9OÌ$`YÔãñaAïýÆiþá@KàÛTòæÈãÿÐÑô¨e@PB{è9ÿ~DíÓ¤†kĵÇoË.ÿL&ÅöD’´×~õ…5$kú™vÁ] eÆ5/1ÿ~¯së‹z(BiéÄn­Ñ<­©Da^ÅŽ{æH"œQêdä†<¢ëîüÿ÷J›GM5â;énAq/"¹Ã[ÿ¡"%ËÓ‹ROYãÈ9s‘çéØäüÄ|ÉÎÎn™&!Êb®UBõ“èSø˜Ô›»êQßÝb›a®úðÑ…5WÏÉî¸ÔždCG¤ƒ7r¡fÀØýÔ•Òg*¤X½ÛNI’Á•Ñ 4ôŒÛ’8C"$æ<ïÑJy6fØê2cæ•ðËøDÝì¡”æ&ùæhóÉ3à Ě奄pïSTq‰]øèBëÐÏ÷„B&ØõÙm¹?¶CÜŸZÇO(êöfÅ ¹`&8†ÅRJ%ÿi%‚ëÔ!;ç½’ÜZ“5! ãÉîú±‘|| 2+QÏ_޲@v>ĈžZîXcÊf¯ïë9_RL˜”#ÂÏØ ø‡…·CÚDÌV„ŸGq\áùGb WÛ ð]î tE³ÅÐê/£,@³>Ã;¢ºn¹ÚOÛxù¡,õ£pÏóŽ@TªÔC,Ø3)7œ:äãè8–ûªÝXÉ{d´ 4ër¤FÁ…­M`6x?E¬_œÌñqè .^bdÑ`óï×rï#çH{8ÌNZ0O8ÿ~k¿‰^—ÿ,Ñľ{)&êœá„äþP"D´xç˜o"[«Ÿ€náæ|ÍæóÕc½î.;œO—W¢ÿ|GÞYʪáÕ*ú3ð”²/™[›¬ÿwøœÉsè÷ ÝêÌ€™±Ú ½PQ¹Þ\¥0}ßÖi\˜Ž,ä5«dOQQ9Ÿeê=³’ÕÌ”wèñdDveŸ%ʯ®M1{R¿öNp‹r{“L^Ô°¹ðò.ަk`RðDÏ1G jöKÜøNËR’ èÓb²Kî$½&ûд+¸ Òܨðý§Kìõ,46‘PÜ“–3„åž™6‰Iå‚pµ†&Ÿ/Z0óÝ{JÉ»`×óÛC|,,ûˆ:‡ No‹óðɯ¡JGZI6y¡7ÿÿHm‰0û(¶ÝWÿàƒïϰU^äNÙ·Z“•ÈfVÿŰ՟¯$I±¡ ¸´©æGðï>~ý̯Ñ'¢­Èmú7Ú*ÊMѧ^M äfœCOk*³L‘ÌÚyàÜÕ.0Þs°&}ZFqb‚RáÇùàçá°‘€äq»ef{üm³N5wI_ÿsLOééÊWÿ{` !X[ 1ÿAJv6.†ê•î‘A‡ùdÿXŒ6\ øÂÝb™+Oü¦åWq«ðwÁÑñ*`¦ýC.Ô@JÙmÈ……!HZ B…¸ºÛ” þZtçñ˜oôêdSÂÔBÛ¬ØOûÃ$ Ø„¤úö–Ù ¶5Sm‰hƼˆ2°ˆü`ñ9FâSäÒhO\îÒÄ4™ÁÞúûPºpDîçÿÿù ÇoïËXó&çßÿ~`—.…¶èg>ÑnˆFôxFñ[ W0‡í1ú´û_DÜï¤ËìXE,NÄ;ôsÇåæäðh#{`û:„l›/yàÐðs¾Ô=ýjàU³r ÷Eó˜ vDÊ/øÉű@!Y1i:3;†à#Ö ÀÎ\³CC9nŒ†DV9’Ó šKÕ†)hQ[Gr«Ë«ïdZÚª™¢•#èŒ'tó#€t[¿PcÙŽåÿÿZ4`¹Üå »Æÿ9kþEù¤G¹ŸŒhÒò±7µ$ÿ]–í·]%Åz™±‰‰ê ¡rÕs!+h»ù vvÌŸ5"èþÒïö—´wöލð1ƒGö™´Ôý¤?´”hÎ7üãOÎ'ó‰ñ4‹«±|ʧ©,«”`€}\Í8“„€Œ$$Àe•Ò D’'lˆÍÏòNô¦gSU)¤À¦-[Yƒ;ømöCx ×rÚø $$Èm²Œ#”ºepŒðñ‚OˆobXcʾ #Íh †˜Uq #¦UQÔðD"GhEb˜Ø_ì0HG¹Ê?†+@a"¢Ž迬m€ ad ªN™‰í±Èc#–^Œ܈ jB8à´€ÂB]êx@‘e)Ü· _‰ŸpmO“JkÜw4À¿býÍ  qþÙ£¨ŒbÌF–˜Úóê x«œ eä­U¸v‰ŸpwðöïW×pÛO …ÔÀ?­^)Oµ!/áóáŠ6—IðD!ž@ðuö4X`üùÁvSí´‘aa’þ<¿ìVU1ŸPË/®M1{QÉJOr{“L^Ô{»ðñƒ}ä´ÁXˆðD"š^ÁYèþÓöœŸ´·ö–¨ü1ùƒGö´ìý§´ìh?¨/ýA‡ê ýA@ñ4 Hî/2–¿gZÞd€Gã¬åaö(æ)ÄlTe`ý=¡ÃýÑÿWRâ<ÐIúu#ÌòNðw¢[ćûúÈ€0íÑèÂ< 1ë7À‘.FmcF|RØG>ò)Pßiðñz«‚|ö$?öOvlÂ8P®×»„V\£îQØG_ÅHúœj¡F„}ÊhðDÚ·až§À0Ž,°kÄažÂˆû”vÔ>§„h{rš#8¹ü¬Bˆõ# \íë.#ÿ4âÌ<ò¡°Ï cÔÊ™~°0p(Lp¨è~§,}U ~(ª‘ÿJoð“D›ö(`EN?ÿ2 ¸ÙØHKúìße*1PFÅ‚ÙEæ%á8q`¿⊩þ¢ÈdÑ&ýðö õzúù¼”êi™—±p_º½Ym $%ã|`õ -" ñEeÜ Ç¢oðDÕz¾¾jðš[ ïÜG¸_t´ Ì¡òà $%ðÖȼMp´À×@]Å$Î|èÅp‹ñEoŸPË/®M1{R¿öOr{“L^Ô°¹òU\\¤å¤Ð" óp<\¥1ñyp±éþ¿ð>?—ð3ÀGøhïü-›í þÃ@ÿCC/´/Oð>ÿAþÀeϨø\?ð¹o´&}@ßÃE?†_h^‹ö‡oÚ~ПÚï´#øU𣯴ûAÿBÉþ•ö‚ŸÑêÄ•b´Ñ(:…Q÷[»- óÐZ_@¿DvvýoúöF„kWj%°F±ÐÊ&0F±[&0F°RUüß4òSrc! 4ºË:wÀposÜ%¹B„Tê±i€‘.Fm…¡"*kÍ´=‹´ãͶð#€ͧŸš±Ï­—Ù"*¨å³Hð #k¨@d¡c?E!„u÷ñ¢œ#ÇËQú)Â:À±¢œ#PCµ·z‰êdð.ôšl¼„køƒa ’øGè¤0Ž®€#ôS„wƪ?E8GO›#ôS„søV"0­Û ·_K-Ñ!¸8Dâ9˜ ¾Ç–;AU´aX‚ãæÏB‘ Ë3÷ôp7ÑËn¦N;ËlÇ©K@¡ÅSq=”¹Ç[Ú`V —”Ž01KÆmtì¹{øoì‘^eÇjܲ¢P¥ú[F-þ \9XïGW7|C€äƒÆÌ›ž¹¢$ L>„' Z‹G±õ£îl ÑH”~‚-Ía‡9ÄÞÒy¬Cƒ†¯)c¢Šäà²tűdW õÊÕ°qÚ¥þ!ØÄ§öˆ}ï•­* Åa“aê"ûKœ2û ªRý¢"·k3OÚX—QÝU'¿bQû½&„,±)~Ñ[µ™§í,K¨îª“ß±(ýÞ“B}ªÉ.ø¢"·k3OÚX—QÝU'¿bQû½&ƒãlW¾ijZ¡ˆÄ¿3Òazc³±éã¶¡Zà?ç¼ËsÂ?àÓå‚`|$4>o?ÿ¶Žqw*ìJ,$¨Fö†Ü-ÄZÃò—Ð×VÊ©iÆ ˆ3²&_è3´rˆÂåE™Ø¤h€Ñ¢ylÖ,°Øv¥ªTuãc}‹°`\~×s—ñÛí_Uµ^™¿ã3÷ì«â¶;:;" Ü HBrï›Ýº×¡184×F%zý[qý{Ã&¾FÆ/bpi®ŒJõú¶ãÔÿ>5ò61xƒMtbW¯Õø•')åÆŽ­qøð½˜ð -ó$FØ@ø{.Ì¢ŸNÕg´ªôN®‹KÌ ¬û à“Î÷,2Àü 2ïŒQ¨FŒI–ÑLIUÂ.¸•™WÉIIÓóèÑùìŒÜ`WÒ¡ VÁÅ DÕÛDb‹Ù–wNóK¹WGªakø–bicï—Å&ÛƒLÐSÿ-_H»Ó-<Êß9€>‡ÇÈ]o¢­ÔÇkx¬ŒAš í¼¨êFŽ5ÏåÜ7—<—]ˆp4˜4ûwšóØwÿè[7;ÔpÀÌÑ ŸœŸHPxêæNOÂcb’Ð'À™íX‘ë;ˆš{|þ:Ÿ„ÆÅ% O3ÚŒs±Ø‰§·Ïã©øLlRZø;–ýþ´¾§e<¿ûp¢^þœf…bUÛáwuÂàWîñfÍüBŽ@Ñø~×aL­Ò0ÐÐ×T+ƒËð4w)ÂþKMeæ¦ã:”!;:r/do“«â;‹ÒÍ•mA:šØÖÀW´èCÇ#|ľÂE 3ÇE’’ý#õ¹ú¿³™#ñØîÕñw[ÁƒÛ ÁKÔãnþý#1%_Ú:aµM ò¸9'ådÅñGÿ…0óº#Ìl°±=‰^Yt)× Ñ|dlOX“^$;Cm+nœÊdõSxÙ8Øž±&¼Hv2†ÚVÜ9”ɇj¥ÿG<:Mxìe ´­¸:s)“Òk2医Yò‘ä= 6WPªÒ¾Tü†//L…¥iwÀLÔÀøË¢àPüA{Ö¦˜ z&ý’ 1è›é-@DÝNpŽ»b´¯•?¡#È=Ñ®ßBÔz’^}@ƒøa{°‘}]«L—n€=~NžôMö·Ç¢nðöª@Óû÷äR úLeû]¦4kã‡'ð úö›ÿyyŸÉïɘÔ#rñ><ƒøNËVdÜÄÆóÈG¸*åšïu³Õ»$##Á±µHù¦ì«U¸JI¦N…>î–”N«©K”ÆSb½6cØ#2ÀŸæ´^ç<, #\N¬ÇHÝcwÅ_bö•óHÔoÖÇkë×D¾" ;^q¿¹©zÚržg>®ÎýGð ±*jë8ÕÜ#q:¬±·œÔ6–msÆC½C°K™µ§öÿtÙB¨©çÅ!û¶î@Ö|1ì• ²i66E‘•Ï0f#o¿íû‡–J…Y4›"ÈÊç˜3·kÜ6Ÿ<• ²i6E=/.sÉÙ*Ý NR°ûrS¥·¿|–igÓe%y0ŠÛtT›Gc³™)òüÏÿþIaÉÒi2¶‡xùH÷±ýd -Þå"3K¡„L…Mù̾Óhgn ZX8ܳRÁè."“ ùñ³×„ úÎV^° ö¯l=;«æ¬Pd¢rêÁ 1–ÃÖÞ‡Ñ)¨¡ÎÝ¿¼?ÿ¾¥ïŠÈ>ƒÑ×pÒqXHKÿ~u„»Xz”ß·,áÖÔEøSËh&ù?šíaêS~ܳ‡[QáO- l˜~–>»XrŨ@àC…<¶”ø˜±ê ¦/yÝb–šîYb=§ Õ}ËÁpÆ&q¡\[€l¨Õo¥~>à%÷5 ÈÊ·Øò9âþÁ ªA:bðSç âŸßD.Êg²/ãVÖ']ÊŠxkè%™ë{…C—“BðT\ÀV¦4ÀØ•q1œã9zB­Íg˶—/ö|ÀØþ§l'ËÒnkB gýí´©T—šÃ‹ôV„Þ½18(Á{]Wv×Þ.º% Lû×$ 2ß5E mÖ"ƒ÷o» áL?V!䥒6óÚ:Àéµê7«#P[ÕI¿·!0Ø ôÚM²ù%YÆ)­:¸v£X½ŸÛ´WòÇAáA¤B—{¯JŸÆÔÿFí¨.©’Z—#šbOkððÌ1&Úޘ̌w°˜"UAè÷B˜dñËÂ}þhé„ ŠºØÛcÏfø°‚ ô¨Â>5A¼cŒ«rÝ‘‚tàgýÕØ–qû?[b¦Íd¡®µl6òC@|©ÙúØã6k … t}¬ka´ísʘýŸ­Ž1Sf²ˆP×GÚÆ¶Ì”w‰¶òj,yñ5¤¬ÿ6ö/òCßñkĤ¬ (v ©èÛðò n½[ –ðDíý[#øjFc#m+áÏ"XÊèÈpÚ×ñwA±ˆ¬X¡œ80ÛÜͤ錀sçLâ4ŒF—qüx劉<½¹—Zõ)Ì©ö~`Ï.dz%K±Ù±S †ôX¸JÔ¼ú¡Qʧ~­ fñ‘€_=qÛÂ5R{µçÿ3ü«/ÒmWèß߀r D”ƒB mñ•ÔƒAÑ% Ђ[#ªTƒAÑ% ЂZœµ¨1ƒ¢JA¡ ðïÛm1(PÈ ä <“£}[áÜ·úwÏø¬=Ïž¬,š)Çðº%Þ¡a€•4t9Û5g—eJç9àYÖǪÜ%`»éÏ ÿêHK°º¾£ð§V¥—ÿœñfºê=Dó(Ђw!Ì1NV¿ÿ{‰ƒlEàP¼Ì©…Û:L¦Á¤#ÔОr,²îîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîã333333333333333333333333333333>îîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîã333333333333333333333333333333>îîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîî333333333333333333333333333333>îîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîã333333333333333333333333333333>îîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîî#333333333333333333333333333333>îîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîã333333333333333333333333333333>îîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîC333333333333333333333333333333>îîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîã333333333333333333333333333333>îîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîc333333333333333333333333333333>îîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîã333333333333333333333333333333>îîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîî333333333333333333333333333333>îîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîã333333333333333333333333333333>îîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîS333333333333333333333333333333>îîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîÝÝÝÝÝÝÝÞîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîíÝÝÝÝÝÝÞîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîÝÝÝÝÝÝÝÞîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîíÝÝÝÝÝÝÞîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîÝÝÝÝÝÝÝÞîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîíÝÝÝÝÝÝÞîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîÝÝÝÝÝÝÝÞîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîíÝÝÝÝÝÝÞîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîÝÝÝÝÝÝÝÞîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîíÝÝÝÝÝÝÞîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîÝÝÝÝÝÝÝÞîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîíÝÝÝÝÝÝÞîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîÝÝÝÝÝÝÝÞîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîíÝÝÝÝÝÝÞîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîÞÞÞÞÞÞÞÞÞÞÞÝÝÝÝÝÝÝÞÞÞÞÞÞÞÞÞÞÞÞÞîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîíÝÝÝÝÝÝÝÝÝÝÝÝÝÝÝÝÝÝÝÝÝÝÝÝÝÝÝÝÝÝÞîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîÝÝÝÝÝÝÝÝÝÝÝÝÝÝÝÝÝÝÝÝÝÝÝÝÝÝÝÝÝÝÝÞîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîíÝÝÝÝÝÝÝÝÝÝÝÝÝÝÝÝÝÝÝÝÝÝÝÝÝÝÝÝÝÝÞîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîÝÝÝÝÝÝÝÝÝÝÝÝÝÝÝÝÝÝÝÝÝÝÝÝÝÝÝÝÝÝÝÞîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîíÝÝÝÝÝÝÝÝÝÝÝÝÝÝÝÝÝÝÝÝÝÝÝÝÝÝÝÝÝÝÞîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîÝÝÝÝÝÝÝÝÝÝÝÝÝÝÝÝÝÝÝÝÝÝÝÝÝÝÝÝÝÝÝÞîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîíÝÝÝÝÝÝÝÝÝÝÝÝÝÝÝÝÝÝÝÝÝÝÝÝÝÝÝÝÝÝÞîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîÝÝÝÝÝÝÝÝÝÝÝÝÝÝÝÝÝÝÝÝÝÝÝÝÝÝÝÝÝÝÝÞîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîíÝÝÝÝÝÝÝÝÝÝÝÝÝÝÝÝÝÝÝÝÝÝÝÝÝÝÝÝÝÝÞîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîÝÝÝÝÝÝÝÝÝÝÝÝÝÝÝÝÝÝÝÝÝÝÝÝÝÝÝÝÝÝÝÞîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîíÝÝÝÝÝÝÝÝÝÝÝÝÝÝÝÝÝÝÝÝÝÝÝÝÝÝÝÝÝÝÞîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîÝÝÝÝÝÝÝÝÝÝÝÝÝÝÝÝÝÝÝÝÝÝÝÝÝÝÝÝÝÝÝÞîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîííííííííííííÝÝÝÝÝÝÝÝíííííííííííîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîÝÝÝÝÝÝÝÞîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîíÝÝÝÝÝÝÞîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîÝÝÝÝÝÝÝÞîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîíÝÝÝÝÝÝÞîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîÝÝÝÝÝÝÝÞîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîíÝÝÝÝÝÝÞîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîÝÝÝÝÝÝÝÞîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîíÝÝÝÝÝÝÞîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîÝÝÝÝÝÝÝÞîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîíÝÝÝÝÝÝÞîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîÝÝÝÝÝÝÝÞîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîíÝÝÝÝÝÝÞîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîÝÝÝÝÝÝÝÞîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîíÝÝÝÝÝÝÞîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîÝÝÝÝÝÝÝÞîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîíÝÝÝÝÝÝÞîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîîÞÞÞÞÞÞÞÞîîîîîîîîîîîîîîîîîîîîîîîîîîîîÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿþªªÿÿÿÿÿÿÿÿÿÿÿÿÿðÿÿÿÿÿÿÿÿÿÿÿÿÀÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿþ?ÿÿÿÿÿÿÿÿÿÿðÿÿÿÿÿÿÿÿÿÿàÿÿÿÿÿÿÿÿÿÿÀÿÿÿÿÿÿÿÿÿÿ€ÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿþ?ÿÿÿÿÿÿÿÿüÿÿÿÿÿÿÿÿüÿÿÿÿÿÿÿÿøÿÿÿÿÿÿÿÿø¿øÿÿÿÿÿÿÿÿðÿÿÿÿÿÿÿÿÿÿðÿÿàÿÿÿÿÿÿÿÿàÿÿðÿÿÿÿÿÿÿÿà?ÿÿþÿÿÿÿÿÿÿÿÀÿÿÿÿÿÿÿÿÿÿÿÀÿÿÿÿ€ÿÿÿÿÿÿÿÿÀÿÿÿÿÿÿÿÿÿÿÿÿÀÿÿÿÿ€ÿÿÿÿÿÿÿÿÀÿÿÿÿÀÿÿÿÿÿÿÿÿ€ÿÿÿÿÀÿÿÿÿÿÿÿÿ€ÿÿÿÿÀÿÿÿÿÿÿÿÿ€ÿÿÿÿàÿÿÿÿÿÿÿÿ€ÿÿÿÿÀÿÿÿÿÿÿÿÿ€ÿÿÿÿàÿÿÿÿÿÿÿÿ€ÿÿÿÿÀÿÿÿÿÿÿÿÿ€ÿþªÿàÿÿÿÿÿÿÿÿ€ÿüÀÿÿÿÿÿÿÿÿ€ÿüàÿÿÿÿÿÿÿÿ€ÿüÀÿÿÿÿÿÿÿÿ€ÿüÿÀÿÿÿÿÿÿÿÿÀÿüÀÿÿÿÿÿÿÿÿ€ÿü€ÿÿÿÿÿÿÿÿÀÿüÿÿÿÿÿÿÿÿÀÿüÿ€ÿÿÿÿÿÿÿÿÀü~ÿÿÿÿÿÿÿÿà?ü~ÿÿÿÿÿÿÿÿàüpÿÿÿÿÿÿÿÿàü ÿÿÿÿÿÿÿÿðüÿÿÿÿÿÿÿÿøü?ÿÿÿÿÿÿÿÿðüÿÿÿÿÿÿÿÿøüÿÿÿÿÿÿÿÿÿüüÿÿÿÿÿÿÿÿþüÿÿÿÿÿÿÿÿÿüÿÿÿÿÿÿÿÿÿ€üÿÿÿÿÿÿÿÿÿÀüÿÿÿÿÿÿÿÿÿàüÿÿÿÿÿÿÿÿÿðüÿÿÿÿÿÿÿÿÿþüÿÿÿÿÿÿÿÿÿÿüÿÿÿÿÿÿÿÿÿÿëþ"""#ÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿˆˆˆ€€€€ÿÿÿÿÿÿÿÿ€ÿÿÿÿÿÿÿÿ€ÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿ€ÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿ€ÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿ€ÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿ€ÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿ€ÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿ€ÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿ€ÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿ€ÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿ€ÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿ€ÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿ€ÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿ€ÿÿÿÿÿÿÿÿÿÿÿÕUUUUUÿÿÿÿÿÿÿÿ€ÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿ€ÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿ€ÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿ€ÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿ€ÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿ€ÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿªªªªª«ÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿ€ÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿ€ÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿ€ÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿ€ÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿ€ÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿ€ÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿ€ÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿ€ÿÿÿÿÿÿÿÿÿÿÿÿÿÿUUÿÿÿÿÿÿÿqgit-2.7/src/resources/qgit.png000066400000000000000000000014631305655150700166140ustar00rootroot00000000000000‰PNG  IHDR00Wù‡sRGB®Îé pHYs  šœØIDAThÞíšMHTQÇSÖ¢@£¤ZDDT‚Edmš„@+¥Qt‚‚6R›-"ƒ6Ñ¡ei-‚ rQ -" ©È"+¥%efÓb®0<î8ófæ>x3ó¿ŸÿûîùŸû1Xô~ýF Ý’t¨+Q̶Êpcs-¼¼Ø M£Ä-$(Ð"y(LˆfɶØmÁ/ýYÊÞìŠÅ{\ªP8‘ç€íÏ!O/Ðú@H ËÇno³ä©ŽZð“À›,e;U¡•jpÇ’TÓ‹?)å)4Zê>ð7$ÐrzøfÁZ*Ô»©z°ÂaŸ^­zôì‹Ë)T \sH ¸.%BSx-4æ‘Èï¹äM†2Zˆ‰H@U“"2˜í³Šª:êr Ù:= h¶5@0Ô·|V·Ë,Ùƒ! "ÍÀ`Q©8qzçZj*4ÞùÀ…"ôápÅ,þÆ€—Î ç¼L/R,ÚԩꇠY#°Ð‚÷mÀ`†rýÀqà¹_ $ÌÀBà {¬WÕvUý3DžÖÝž´eÀçŒÎ×xà¯@£ªåR‡Ñùf3çÓ­5ˆ7°Øœ.©ê€ŸJTµxè—AÀÖÈÓ yG€wi×IžßsDd¦kúeÁ*2Œò]`Éuý´`óÉ~‚]ж`+óñ®§ÐG‹ó5å#@½ûQš„eÔŽøT³`µò‰ó7,ØiÙ›‹ ‹ÈN³|ðZ·ª&];1@'©‹ìrÏ`\‘ÃæTá}Ú3¬6“º¥©ÊPoG`iȧÁ ¬CUÛÔ«j'p¶H 워S‰6à|u$€ZUž´=±ˆl7NY飨p 8ç×qlêE¤hHýÑ£,C¼Gêö&®ªŸ¦Ü©„!Sa¢lÒì¸Æ??«jQïþɽÎ`*c¡IEND®B`‚qgit-2.7/src/resources/range_select.png000066400000000000000000000020521305655150700202760ustar00rootroot00000000000000‰PNG  IHDRÄ´l;sBIT|dˆtEXtTitleMade with SodipodiŸô/'tEXtAuthorUnknownŒ! òzTXtDescriptionxœKT(ÈL.)-J_~êô ,yIDATxœ¥”=lUÇïíÛÛ½óÚw·{Îaçü…m ó!(‚B  ‰‚Ð…ˆÂB„ M@A¢•{†"‰Ý  ¤ D;ç,L0 NbßÙ·C±ëØî’c¤ÑöcæF¿yJOÊÛ¥ã~†½]L|xXÕy]†`ÎÏSV  Ô¶[€m±ÀÖ ä4„ (ž6}eÞ:t?öKÂñ9‘ÑâÚóÀ»Õ/L쓱ûza q ­"0†(N5h†°ÂF n¶¤ªÃ•¦ä Gqzz‹à5ºt`ÿÝ€•y“¬h’Æ!4[psaú-Ì>0€X¾ê7å`W=\ÏicY>Óc‚Ìž‡Z<ú*iWÿ‹ã.‘]?TCY—RŽ;rÌI,“.úÇË:›¹¥w0œqÜ_J6¦g¾_Rã¯}à7.žÞïÀ¡zêÔ?ÀO@ APlþ’Ž£»-;ËIEND®B`‚qgit-2.7/src/resources/reload.png000066400000000000000000000016161305655150700171160ustar00rootroot00000000000000‰PNG  IHDRóÿabKGDùC» pHYs íÀ,tIMEÓ ÂÒA¢IDATxÚm“Ëk\eÆßw¾™93Kf&䦉6 FC5¯]H¡ZŠÝD”. .DÚn\ºwú'¸,.ŠD”ÙH¶Œ—ªdššE%f’Ø6·I¦gngæô\æœã"¢ŒøÂ ïâý=ï¯àjöòªÁ£‘Nh8^Èʆŕ›mÊ÷Æýý·~¶T,Cíþ-‰·f§G¦>zûC ËvÙ;4±=BÁ@_„WŸÕiÙZbm_œëªüÕ¹qajÃó?8÷þYÄ4ô§4¦ŽçQ¢KÍô1…8nÈÓ£16´D¥%RãñõëÚ쥒ŽûâÍ“=€mž?1B*©×£4¬€]ÃÁr|,; ïr{3œòÝöUÌGõd&÷©^mŒ ¹^j°]uHÄ$¶‹Jr)Ÿ6±´žEÆÂdËxY“¹¤¤ÚêòÔp €Õ-‹ò®}ä@¶™ÍðÒ‰<•G°´2¢#¤šT€p}†Æp.ÀvMq³ÜÇ+ãm.œïÉÝB„@J ÁfÓ Y3úùyó(¶ÓÏ)FrŠ•Ý>jM»Gà æ¢$ø]X;JyF©ë$šR e6Œ$7VNÏÄøô]C£E.“ºR´ìz'ç¯Ý‘3²Ø¾uE:ÆÒ-¾¾eqmÙÄvòi¼nÈw-v Û ðéTˆt«ß ¨û{à…÷¾Íú‘üò‹Óý¹LôŸkQ \ÿh‚ˆTë.¿Ü­îŒ87Îj.kgçâv=xìÚƒšöFR—ÙÇûcÄ£’ˆè‘{k¯Ãí5cgØûñâ,—KXYYáÌ\²ÞôR_m×Uj«âNÙŽµ½€FÛå޾ɯ¿7;{ûÅ!ïûþ†[…B¡÷Èf³Ñmwb¬-Ÿ8%TâI¡Å\X;9íN_P®š¦ib±À_`='µZ_IEND®B`‚qgit-2.7/src/resources/remove.png000066400000000000000000000015701305655150700171440ustar00rootroot00000000000000‰PNG  IHDRóÿasBIT|dˆtEXtSoftwarewww.inkscape.org›î< IDAT8m“OL“wÇ?ï۷о´o»2*¦i^P«BkRRãE\L…ì`Œ7o$‹—ÉÍyp]ˆ311Äìäq l‡]\¢ h!°‘f“µ‚F,ŵHmßZÞ¶¿ °}“ßáyò<ßçy~Ïó•„Ä„¦©>]³(J !zMEÉõz*“ÍŽ?*K㥃ãño0øàÔÕ«v_?v¿„ ’Ë‘O¥Xž­¾ÝÙùêÎÂÂô¾¿pa&œH ‡††$ 8Ò™$ðÛô´X^YùñÎÂÂò~åÏu}8pù²¤>½«‹J¡@es“Êæ&åbGw7ºNïRÐáþ:'4M5 …ûZ4*uÅbXm6Ô@€Ž“'ùP,R.•ðž;‡ÚÙ‰Ãíæ³¾>¼Ñ¨d/î'4MU|º>öqqÑYÛÝ¥º»‹ÕnÀ Q3 ÚœNÜ@k”Z¥‚i8³Yç™XlL±(JL¶Ÿ?g±Ùäü­[xŽ pöì¡È¿|ÉêÔ{óó´.E°$|¾o[[F.‡¼·Ç›t_$‚Ãí>”ü&fy|ëü<•ÕU,BðÁç³(Íš¦I>™¤½Ù¤úþ=ƒ‡v^¿ÆL&i[[Ã% `dl¶ ²üiU== >|ȉH„£è»r…î{÷({½8§ÅBÃfËÊF½¾hu¹¨ûý =yBh` •ô×ÜùL¦eÇ®_Ç79I]Qpª*×ë)¾ôxÔápùñà øhb>{&fC!ñC4*òëë-ÿÛ ñË¥Kbª§§õxTùQ±X2Žmnm‰™Û·©U«¤Ÿ>å÷›79±¾Nde…äµkä_½bçÝ;þ˜œÄžË‰5M].K­Sþ6Ÿ ˜æpG8,Õ—–ð¿x Ÿž,³qñ"íýýHssâ'›­uʇÄt7é¬V¿;£(ÍëÅV«áPìííÈÛÛüº·WI:£ÿ'¦}$4M=¯ëc^«5æ2Í^ PµZ39ÓLýœÍŽ/‘ó?+¾[ ¥ó¾IEND®B`‚qgit-2.7/src/resources/shellscript.png000066400000000000000000000013331305655150700202000ustar00rootroot00000000000000‰PNG  IHDRóÿa pHYs  šœgAMA±Ž|ûQ“ cHRMz%€ƒùÿ€éu0ê`:˜o’_ÅFQIDATxÚbìïïÿÏ@ QPPÀpìÖ†o?þ1|úþ›áí—? OÞþb¸õô;Ã÷_¾þÊýúËðí'ˆý›áó÷¿ Ïg›2L˜0 cpİ ‡k¸û¯¦\[A?yä¯4G@õÒÜ %¬-Hmˆ¾Ž’ùŽ<Œ± 0À¢] Bÿÿ³£¢­‹d $3À/ލþ¨þ£Øí¥hr%(Ž›ì ¸?~jüÇðãÇNNNfff ÿþþý›áÅ«· ʼ`1€‚ÀÆÆÆ°}Ç& Æïß3|ùüE3###Ç<==TUUáâ7dã/  J rJJ ½íí@ÿe`ÚÒ  Z[[ƒ]„â‚·oß2¬^»–áÝë× ’ÒÒ ì 7¯]cøõëß?¾|ùÂàïï7n ÓGvmÛÆ ""ÂÀÉÅÅÀ ûòõ+Ã÷oß>½ôýûw°AÈ ²¹€šxyyÁ|x÷ì-AN K@lyÙ€bA(qqq)))°a Wüû èP¾ƒ âÃ@Á 91!!!<<¬é?0ñü"ÈÉ >Èÿ€^sçÎ1ÜPü‚œòH3 ü÷éÓ'° @”Œ ÙnÈÉèñLÀԉ̇j J:À@©]3 LX€¬ $úYâÔaQIEND®B`‚qgit-2.7/src/resources/source_c.png000066400000000000000000000012071305655150700174460ustar00rootroot00000000000000‰PNG  IHDRóÿa pHYs  šœgAMA±Ž|ûQ“ cHRMz%€ƒùÿ€éu0ê`:˜o’_ÅFýIDATxÚbìïïÿÏ@ QPPÀ°jóg†'2üþúá×7 þú—á'ƒø úß_„=ÿþ10ÌÚ Â0a€b z8p1üýËÉð¨î?PÁ †¿@• úß?þ ”ƒ°ÿÿC@pøx™±8¤ðÄV ©ÿ úï?€bÁå·¯_¿2¼|ù’áÛ·o ,,,  ¼¼¼@C@®øÇÀÁQ@LØ4ùò…áÆ Ÿ>}bàææføñãõk×Þ½{ÇÀÈÈv V<}ú¬PCCƒ ìŠçÏŸ3011A¼ä€Â0¤àû÷ï`›ANÿýû7333ƒœœ˜ ’„aÌô?þ€ý ù Ÿ?»d(2 ¸Q ÿ‚0H1Ãû÷ïÁdÀÇÁaâ³²²¢¸ €àÆB¤@GG‡AJJ pwïÞe`gggøùó'Xd(È ¸""" üüüàÀÙ¢¦¦vH3Èé ƒvî܉à7drœ)((PÀ‚@xx8˜%ãÂÂB€bA@dÿÁÄ@†À BÒÌ d~ ¸ gã hEÒ Òv*@±À‰ÈšA À‚ ö©:¬IEND®B`‚qgit-2.7/src/resources/source_cpp.png000066400000000000000000000011561305655150700200110ustar00rootroot00000000000000‰PNG  IHDRóÿa pHYs  šœgAMA±Ž|ûQ“ cHRMz%€ƒùÿ€éu0ê`:˜o’_ÅFäIDATxÚbìïïÿÏ@ QPPÀÀ°}6ÃÏŸ ÷o00\9ÍÀðùÃÇÏ@ú#ç¯ ¿ö0BuýâÛÿ&L˜À@,p£¬~ýb`°Ê222üÿûˆÿ‚ñ?ýÈÿÿHñÿ œPm„0€_Îü„ÿ0Ö Äÿ Á@±`óÈ¥`[ AlF&  00|”c`c« þüùÃðñãGVVVnnn†?~0|ÿþ‰‰‰““D?Áä °€‚|øð!X“×/_À¶ä@†‚,ñAÂ@Á øôéX¡¬¬,X!ˆ cff{å/ÔK ÍŒŒŒpnH!HÑ/`L¼ÿžáóçÏpoœ+&&@÷` €à.I€‚¼R(**  ...°˜ÍÏÏ7 €à°°°0ˆˆˆ€]r"ƒ4ÀÂÀ}ûö V @p`2æ"äÀìÎ;1¢ €0 €§¤€Bf‡‡‡ƒiP2.,,ä œ †,Õ JI% ðXÈC5Ã@,0Ab²f0X„߈ß5‡ËIEND®B`‚qgit-2.7/src/resources/source_h.png000066400000000000000000000011641305655150700174550ustar00rootroot00000000000000‰PNG  IHDRóÿa pHYs  šœgAMA±Ž|ûQ“ cHRMz%€ƒùÿ€éu0ê`:˜o’_ÅFêIDATxÚbìïïÿÏ@ QPPÀðìtÃÇG'~ÿøÂðûë'†ßß?2üú Ä@ú÷·Ï ÿÿþc`„júÿÁkÒ† &0 Ì$Ae_~O ä_ þÃðï~ƒñ? f†ÿþÿÿ¤þåÿÁ]@p8…T1œ÷†ÿƒõƒmþ÷b  6}ùò…áÉ“'  ¼¼¼ ÿþexþü9ï_¿$%%!°ñ‚Õ6~ÿþÍðéÓ'0 v PÃ?¾~ý fƒ „€bÁºLLL þüaøùó'XH#H Y3Š EŒŒ°Ñ/^¼`xùò%˜2Œn8  ²fd6‹ˆˆ0ððð0ü†:È !ÌÌÌ( rÀprrÂÉÊÊÊÀÅÅ6¤$ÒŒl@ÁYß¾}c¸ÿ>˜†yÝe ƒA1  ¸ @NUPP`àààkRRRûd 2\¸páÖ­[(@p@ÎeccƒÛb#,,ÃÃÃÁ4(r r´ÁBÀ\€,ÕÌ dþ ¸,,,xs(E"i†›@,0Ab²f0šŠåÄuCÃsIEND®B`‚qgit-2.7/src/resources/source_java.png000066400000000000000000000013721305655150700201500ustar00rootroot00000000000000‰PNG  IHDRóÿa pHYs  šœgAMA±Ž|ûQ“ cHRMz%€ƒùÿ€éu0ê`:˜o’_ÅFpIDATxÚbìïïÿÏ@ QPPÀpøú†o?ÿ2üù÷áàÕ/ ï¿þføþóן qþ Æ>}ÿÃðb¶)Ä ˆf’¾<à ºÇB…•áÛ÷o ¬lì ÿ€ÿ€†þý¢ÿ2üý ¢Ž ¸|\L`úïß¿ ïÞ¼fàäbàåcŠücøÔøï/þÏ ÄÿøÜ€bA÷X ÃÏ_~¿{v; Ðð`y%lõ„bÀï߿޼yËðúí{†?¾2°5qqs3ðóñ1p°³3°±±A¼4Å€'OŸ1œ>²áûÓK |@M?ÿ13|ýÍÈ ilË ¨ªÁÀÄÄÄÀÈȈâb€bAvú‡÷ïv.îaøýê'¿û¯î½úÎðîÏd†öÞÉ öö`—€¼Ä„lš„¤$Cd^3ºÃÃÏl ?33°‰(0¸û2 3|ÿöáýû÷`Ë` €àFýüù“áïŸ? v®> Žþ ÏŸ?gx÷¨¿~ùÊðâÅs†{÷î1((( ¸ €˜PBˆ=zw¤¤ 33ÃçÏŸXYYÄÅÅÀ „ˆ @úþý;ÃÉ“'~ýúÅðåË0--- ÔK—.1\»v %n3Ð&AAANNN°! /\Ä 4...°„‡‡ƒiP2.,,d dÛAŠ988À¶!áȪŸ7%`˜˜0rÈèšA €X`‚ÄdÍ `M|…\o±LIEND®B`‚qgit-2.7/src/resources/source_pl.png000066400000000000000000000011401305655150700176330ustar00rootroot00000000000000‰PNG  IHDRóÿa pHYs  šœgAMA±Ž|ûQ“ cHRMz%€ƒùÿ€éu0ê`:˜o’_ÅFÖIDATxÚbìïïÿÏ@ QPPÀ°åæO†ã÷~0Ü~õ‹áÇÏ _¾ÿeøòã/˜þ ¤ÿþ©dk±Ÿ÷(1L˜0 €X`&i‹217Ãßœ ÿþýgøó„ÿ1üù¡ÿÿÿÄÿÀr 6 ÜE!,üÁ üïÃ_ ›. º`ÌÈÈVûêˆþû÷/D-@¡ðãdžGAûŸ•••ABBè? /^¼³™™™ÁÁ@1!2ùóçÏ  ’’’ _¿~kðå˰<Ìp &tçCœýn ˆÂLLLpÍ WÀ@¡xfò»wïÀŠxxxÄÄľÿ± h Š nÀû÷ïáâââ \\\(ƒðïß¿á.äææËÜ€>0 2°±±m‚)+Ú ÐgÏžÁc¤n(„AÊÊÊ`æÙ&--Ͱ{÷n¸«nݺ¦nrÀ` X«@ <<Lƒ’qaa!;@Á €) d’fÀ€‚ò'>Š$ÍŒ0q€b  5ƒ@€Ý"!CIEND®B`‚qgit-2.7/src/resources/source_py.png000066400000000000000000000015521305655150700176570ustar00rootroot00000000000000‰PNG  IHDRóÿa pHYs  šœgAMA±Ž|ûQ“ cHRMz%€ƒùÿ€éu0ê`:˜o’_ÅFàIDATxÚbìïïÿÏ@ QPPÀu¾˜áý‡¯ <þgabd`üͰíâ†+Oo30±°2üûû‡á?оÿþϽÄ0a€b™T¬šÃÀôŸ(É”ÿÏÀ „?~ýbH· axöê5à ”a`eeaøöó;Ü7@™G«÷]9ɵ¢–áÏÏÿ NšÆ )f 6òpy€bB×ðãÛ†7ï¿0üùöá'Ãí—ÛÄ1;¼d8ʰŒ!voCñé@¸z€‚»à?Pý‘[{Ö½œÇpîÓ-‘_¦ /.þgxùýƒ¬Ž4“èkE“ï ™¾1üüzn@Á ØysÃyöV†ŸZ?Ì>+0œ¿áÂW6†x„¥>1˜é|fxõ‰‡áóÏ L|¿àÜ€¿lO"”|¿Íðÿ5?ƒ¦Â>Íw Ûr0ؾgäÐf`â’bøËÁÍðúÂË7àèZ!1okAæw B’OÙ™þß>Á°öΆ­šžœ{Ä Êú‹Á€O—áõcN_ˆ>€‚èÍðù×_^.I†·/D^¾’f(Ÿvá·é[[çg †r/¾þ½Â°ëú>†«Ÿx2Áún€¢œ3$ °Ê0¼úÆ mÐÁÀ`ÿ–áë/v†_¸¤ù?0¾+ÆðC’î€BÄ0…ýùûÌþýû'';ƒ¾¦:ÃÑ_wÞ¼æaªÔÛ1ƒ! ¤à5ÃÊ•+Ájž˜€¶Ã0##;3ƒ¯=ÃÃgÜ ÀdÁðü£X]xx8?þœ¡°° €°6\Ô¬X?‹1¼þ Ä·ÌpgƒòP3ù €à°°° `P“•gVöc`þÏÏÀÃ*¬™äS €IÍÎPÍp`Vkó㋤£IEND®B`‚qgit-2.7/src/resources/tab_remove.png000066400000000000000000000007271305655150700177750ustar00rootroot00000000000000‰PNG  IHDRóÿagAMA± üa pHYs  d_‘yIDAT8Ë¥?K›QÆ÷Í%!‡ÐOP¨d(Ø/ÐÿŒ:tºY ~ Ñ­Pº(‚XÁL dê’©B*t("FÿÐWT„¦IƒåMªIMŽƒä5’4 ñ‡ûp~çÜÉŠm Múú"Åå÷ÞÚI°¿ ùsjß’âyþJýÙÙ–ÁÅwX'ÇÝæQõã=QŸáÌæzàÖÁ.êä”òø†fæU'€`=~ªþ¼DÞÀúqÄÕèë®Ãw_ê–~ _„Â*W¤Y•ï_Åûé=êgžÚ“gÔÕ ¾Õ5.§¦¥'JímDdbXªfào4*âóIÝï—žB¬¬'EÙ‡L¦Ò®±òb ÏÞ.K ÀÓ‹Çã·cDDÄq·‰„4†:ùº™Ü> µœÛÎ÷c$‹Q*•¨V«nƒA´Öh­ç:ú b¹\nÙØØ‡ÿ¢ˆˆmÛô+ÍfÉd2ýM$é `ñ@ÝÀsÅÆã¬IEND®B`‚qgit-2.7/src/resources/tar.png000066400000000000000000000015371305655150700164400ustar00rootroot00000000000000‰PNG  IHDRóÿa pHYs  šœgAMA±Ž|ûQ“ cHRMz%€ƒùÿ€éu0ê`:˜o’_ÅFÕIDATxÚbìïïÿÏ@@ÛÃÌ7qWNÉÌ;¨7ôFÔ(mRDYlÙÛᬉǤTãnI/Öó¾¼Ús½äñà:vT•O±ÀL2P`fø÷Ÿ‰ÁX‰á?ÍÈðëË+†?¿ÿ30s+1üùó“áïß¿ ÿþýcøûáh€‚ÀËÉ$!œß_~¿¿ÆðïÏS â? ÿª0pIè3üûûhÿa@± ûç߯¯ ?__`øÿë« »;·0×—w^ÞúÀÀ+gÉÀÈÈÈð÷Ï_6ˆ€‚ðÿÏ/†ï÷×1°±ÿb`æäc`øñŽáÇ‡ç Ÿ?¾e`æ“b`üú…áÅ¥ "ž ÿ–€¿?˜Yÿ3°ò‹00¼{2‘ùû{ö?Ÿþ¼ýÂÀÆÄÆÀþå.ó« Òpa(à€^`ø ä°…ß<``øø†‹‹—‘åó !Œ ï¾-bFø €˜àþ†ì¿__€NÅÀ´õ?7?++#Ðé/€bŸ€áËÌ🙠H³ €kc ¸Qßþføó ¨ãÃßÿ ŒÿÿÕ2Ùl ì¼@q^°ßÿ~F+€‚³Xعþ:3¼yú‘áÿw ­?ß@¢• \<@3ž=ûÆðŸ…›áçÏpî&Æ Ê ØEߘÁ Åu‹…O˜˜^¿øÃðê—(Ãê >1ÜÞ¼n@Á]ÀrÖ¿_ BrÚ ¼&u ¾1<~ò—áîg=†ÿ É ræ±`uááá`üüùs†ÂÂBn€bA1šø…EXmª¾Ó€„„+ ×/_à¶‚òP3+ù €à°° $J^^0†...dÍð¤@,0Ab²f0¾5m¯ù>IEND®B`‚qgit-2.7/src/resources/txt.png000066400000000000000000000011311305655150700164570ustar00rootroot00000000000000‰PNG  IHDRóÿabKGDÿÿÿ ½§“ pHYs  šœtIMEÔ nëð&æIDATxÚ“ÁjSA†¿™LšÞ´ÐBº)Z .,t¡kZ»°®|7‚oáÂgð5ÌÂMÉ2ÅXè¢DZh¥”´¹7irçœã"iÚ4Ä33çÿÎ?Žk6›+­VkŸÿXÕju)xï»Éò3¢(¿Ïr¾öÉúq¸ÏãeÜtGç÷O!,3[x¼ZÄ Ôf0›CUUDQCTQtï}> ˜•ÄOØSuðóÙqàäÇ KÏß`fC°8öFycÀÕef˜æà>RÉSÄì ŸÌcfˆÈ8×_˜f†;ü@aÄ[¤²8Lò›`ŸàÔ!Gó„G¯GßRbŒxùÝ ªSÕ9®á}Ü2§z›ÒâÝqå«â‰\€Ì ë&hš k¯‘áý5ñ Ë2œs„ì+¥RRå,eöáD”,ë1äT«•i€s€b¯ &eÎW)ò‘0ŽEÝîù4àÂ^~Z pg“_Mú÷×ÉÚ)»»_¦ìw:rptdøÖ7ú+Oñ¢8çð~ø¶µµ…s¡V«±¹¹¹?å s’3¿þ‚biŽ ”˳$I €^¯GŒ‘z½Îöö¶»±÷^¾ýëô¥iJ£Ñ‹ÜÎÎÎJ»Ýþçq¾*øJL5øÂá0IEND®B`‚qgit-2.7/src/resources/vcs_commit.png000066400000000000000000000025131305655150700200100ustar00rootroot00000000000000‰PNG  IHDRÄ´l;bKGDÿÿÿ ½§“ pHYs  šœtIMEÔ 1ÄŒXØIDATxÚ•ÛoUÇ?gΜ™í^º»eiB¹ÓðBˆÑ O^xè#ñ–ã_àƒ&šダ‰Ñø V£!ŒJP#S EÚnwi·¥BwöÒéÎìJËM£ßä›_ò;ÉgÎùïÌRwwwØ‘N§·¶¶¶®I&“Ë ÃH!"aúZëêÌÌÌ­‰‰‰üèèhŸÖú0zòäÉ8â.LضýÔŽ;Ž577ïO¥R1­5wîÜ¡\.Zk´ÖH)±,‹0 )—Ëu­õÙlö”ã8§Nœ8QÝÝݪ³³óý{÷>wûöm‘Íf™žž& ÃÀ²,lÛÆ¶m,ËZ°R )%¾ïãº.†aàyÞõ¾¾¾ƒ===ƒ¦âÙ®®®çÏŸ?ã8G ÃX°”Ó41M¥J)b±©TŠÙÙYFFFÚ;::z€¦RªE)E­V{xÜ@ÿéóðyÅãq\×m¤‰DŒŽ9ÂöíÛ‰Åbü›„Ø0 ´Ö”J%¤”¤ÓiŠÅ¢0 ÙL†d2IWWûöí#ŸÏ355E©T¢Z­ÀÂNµ¡¦”ÏóˆÇã¬^½šÁÁÁ9p¡Öšz½Žïû†×u1M“L&ƒRj¡Ÿ-Z|ðë&*Þ /í½Î¶•M(¥(—Ë”J%*•J`ú¾ÏåË—immEJ €mÛ,Y²ä‘d§ý¼šwÚÌú6¯~ÚÉËv–¶ä år™B¡@†ÀˆD"¢¥¥…r¹ŽãP©TÉî #è9ÝÆ Oǘ*A¹¯t5òö¹6® MS«ÕˆÇã455` !ÄŠ+H¥RxžÇøø8ýýý ‰‰D‚ÛAšÞ¾}ÝßÈ-¨rÎÏìIóþO[xm÷[Ö¯`rrrnƵZ­^«ÕBljF£D£QV­Z…çyÜ«óñÅ]tín¢àUÐ0ïùñ‹yó’æu¥X,†ó©0®]»F†d2™…œ¦R)†Æ5ï}߯c›)3€­æ*æÝª`íæfÞ¸Xå0ÖÜŽ¥”¢½½ÇqÈçóÄb1¢Ñ(ÓÕ/žÚ€ã7S„åKA5Ö=kÔA‡P kVqü¯ƒËØÖ¶ÜtCCëÖ­#\×ellŒo~1˜Ô{ ãÜr 1 ‰F°Lüª>ðÐbëò$g/2K¥Òh>ŸgýúõضÍâÅ‹I¥R´.¯ÑôÃn ßÂq—R)š&Åðï]&P¯±ëÜi¨OPv¦s×®¼5$‰Dÿääd‡Özc,J)„˜¶mPìÙ%i ÎzmÐ(! ĸWc`à³gà žÚ¢‡³—{ä¯'s¹\Øßßß;;;Û7<<œk·ÇÁu]<Ï#s†sVûø>¨ˆ‚Š‚…OØûùWçüäèÕ«Wê&€çyÁ™3g>zW®\¹tóæÍ;ÓéôVÛ¶×Ú¶Ý2”[Ôʇ6‘"€ Òu×M__õ[=;|cþm5ú€Õs¹ÜH.—¾¼Û“4?Àa¾&† æ}PeÔs©¸_&ÿ­:u3+Æ&Ù¸È4 Pâ¾k¨LÚ¿9õÈ?ïÿév¸ð$øö£k…)¸ø;ðç|ço¶ ü’àå§IEND®B`‚qgit-2.7/src/resources/view_choose.png000066400000000000000000000012061305655150700201550ustar00rootroot00000000000000‰PNG  IHDRóÿabKGDÿÿÿ ½§“ pHYs  šœtIMEÔ!)§‡KIDATxÚ¥’ÍKTaÆwæŽÊÕŒm I[Ø¢   l线B ’ ·ÑB!\¶hÕ¦ÿ ‚À}!}Ñ$™zçÞ÷ãœÓâ^\̪góœ8ïóœó&÷_|¿}f°>lQ …à Áç±d'¸nÀ»Óõèñ9Rìï¥Î6^7Ç‚3òÜ(r%ÏŒnWÉ»F7Qj‰¢jø hÍÐT1sàv>¥­æÀØh«IQ@6ùduÈÒ’ ¤ Ô @A ®3–ŽŸ3Œƒ DØ‹C¬ò>1*†Dåå›ik®pílóîè*S“mþ…ª¡ªˆ”£²õ>!Å €bã éuv^fj²ÍÊÊ ý°¸¸Œª‘šÚ©FÒ¾2ž%Ë2Ì@U13#Mˆ(ªÚ°}󇇇Üðm ¬mnnöU°°ð¸f•…{MJr¡xÌŒÕÕµê¥Òwo†÷¡²P o—±½4¦×Áî b¬­­öU07÷¨RPYH†ZмˆÕ±jëóó …ÃìX…af$I½Ú‰õ®ðãÖSœsŒŒ\‚¯ˆ(Ïû*˜™yˆ=fvâïîÄyT¥¥'§vpÜWU‚¤¿» ­·±!¥³¯¨xv>ˆ(¥dQÔ SC­ü‰ŽéÖǃg;_ˆ>]$úPÆ>">\™K”ãëžàÛ/Çã/Êu¦zNs„nIEND®B`‚qgit-2.7/src/resources/view_top_bottom.png000066400000000000000000000007761305655150700210760ustar00rootroot00000000000000‰PNG  IHDRóÿabKGDÿÿÿ ½§“ pHYs  šœtIMEÔ"9‘Þì‹IDATxÚ¥’ËjTA†¿s¦g"ãuˆfÈέ+¾‚KŸÊmÀµëlܹ‚ Aƒð‚:ä\º»..z.gшEýÕÍ×]]ÕÕ£gŸ\ßí»¹WR¯¤NJŒJl3)n¯KT4uhÿã[¸sc|r¸?çètÓwF×8mkt­ÓVF]fNʆՎÃ=B<{fÓÉüp6¥ï¡™@7fM(q\A¨ v@šéãÅ<Ýr)ˆ‚*¨lt–e.²#â˜:*Æó5av îßå¯f昪%Ч¯+jܯûÞh§vó+Á\Á†jÉÿx€»ØuÃÝ ç_:Ž_~à_­Æù¼È„ƒÛž<<øÍÍ%®ÝsÇÜ4óæÕˆ0®áÞÍñ¼jPéºaÆòí”5œ¬Ë' §0„ ¸«m°_ªY7q÷æí ¶a3cÅ…ï—™w ¦ås¨¦Š.µŠnÆ6è…Šñó2Nß.žž/$H$墓 Iȱä* ;ÿø5òßö pGÎ$Mð(•IEND®B`‚qgit-2.7/src/resources/view_tree.png000066400000000000000000000011411305655150700176320ustar00rootroot00000000000000‰PNG  IHDRóÿabKGDÿÿÿ ½§“ pHYs  šœtIMEÔ&1ûª“ÚîIDATxÚ¥’?OTQÅïñv—<…eƒ( 1–RóŒñ‹ø,ü–ÖV6–Ö4ÚXÐHÖ˜bDteaa÷½ûgf,.²»*§™{&9“sÎÜlûõ­¹ÖLÛ¢jÁׂ¯bêNp£€wÓóèñRŸü)îÜj¼]k—ëÁUeÔ•R ÑH©FÆ(SòLQ5|P47´P̸î·b±l®¯-–Ô5 ›P5a8Ã"õFE¹(H@í׋ãéx/ýì±ÜYA"D‘„#ø`Äh¨•w9Åâ,<\†ªòœt?³õh…ëJÕPUDRQÙý˜Q`ÆMÞ?C| ò]Ø@5å‘…éxA¿ßgoo€N§CÛb£ßIA dEDQÕé­V‹¥¥%ʲ¼bA/lØ¥3£° eY²ºº €™!w0ÑKÂ8»P`ävMfccòä,á¤@ífòîs²_ŸÈ âö+tîþÔ%Ìlú ƒÁ€n÷ fÆüümÚgp¼OX¨¯X0Ì+h4ln>˜ Ê ²‰'-üãýóÀ×Ã!*ŠÍ®¤pDè)G÷^ w*Jì- ½cL µôOÏÅîþàe÷`@ô‘è"чôöñ‘à–(p%ïï=Ç×_2ÐC®ïhñIEND®B`‚qgit-2.7/src/resources/wizard.png000066400000000000000000000013571305655150700171520ustar00rootroot00000000000000‰PNG  IHDR„b½wsBIT|dˆtEXtSoftwarewww.inkscape.org›î<IDAT8•‘KH”a…Ÿß™)oƒI‚¥˜™ºP —¢é¶Et¡U´ˆèb!Q .ZXh™‹¢JJ¨èFb7i ´ ÆŒIŠâˆéÜÿ™ÿ´1µÛ¼pßË÷>ÎA¿d>µ5†ï¬¿àÛKJâþoZðˆ=*» à„O6÷¾9‚Ss)ŽŽ¶…ûy·€ñG€Ù³¬AÑi)øQÑÁ­ßLïö+Ôb)vQ“/ŒV`5`K¼±“0rDg1¿BJ:ŽuÇÖ`Ì®Á6K<ÔhjÑ% ()΂# ÿ)ìæçÂÖïôŒ"ƒRø¥~(EoJñ¿KíVÁÒl0°3>½ï»¬ORü“d¾’ÌG’yKŠ×Ka‡B_ñv¢ì·!FïÒü°öatrÇD #include #include #include #include #include "domain.h" #include "revdesc.h" RevDesc::RevDesc(QWidget* p) : QTextBrowser(p), d(NULL) { connect(this, SIGNAL(anchorClicked(const QUrl&)), this, SLOT(on_anchorClicked(const QUrl&))); connect(this, SIGNAL(highlighted(const QUrl&)), this, SLOT(on_highlighted(const QUrl&))); } void RevDesc::on_anchorClicked(const QUrl& link) { QRegExp re("[0-9a-f]{40}", Qt::CaseInsensitive); if (re.exactMatch(link.toString())) { setSource(QUrl()); // override default navigation behavior d->st.setSha(link.toString()); UPDATE_DOMAIN(d); } } void RevDesc::on_highlighted(const QUrl& link) { highlightedLink = link.toString(); } void RevDesc::on_linkCopy() { QClipboard* cb = QApplication::clipboard(); cb->setText(highlightedLink); } void RevDesc::contextMenuEvent(QContextMenuEvent* e) { QMenu* menu = createStandardContextMenu(); if (!highlightedLink.isEmpty()) { QAction* act = menu->addAction("Copy link SHA1"); connect(act, SIGNAL(triggered()), this, SLOT(on_linkCopy())); } menu->exec(e->globalPos()); delete menu; } qgit-2.7/src/revdesc.h000066400000000000000000000010421305655150700147250ustar00rootroot00000000000000/* Author: Marco Costalba (C) 2005-2007 Copyright: See COPYING file that comes with this distribution */ #ifndef REVDESC_H #define REVDESC_H #include class Domain; class RevDesc: public QTextBrowser { Q_OBJECT public: RevDesc(QWidget* parent); void setup(Domain* dm) { d = dm; } protected: virtual void contextMenuEvent(QContextMenuEvent* e); private slots: void on_anchorClicked(const QUrl& link); void on_highlighted(const QUrl& link); void on_linkCopy(); private: Domain* d; QString highlightedLink; }; #endif qgit-2.7/src/revsview.cpp000066400000000000000000000222551305655150700155100ustar00rootroot00000000000000/* Description: qgit revision list view Author: Marco Costalba (C) 2005-2007 Copyright: See COPYING file that comes with this distribution */ #include #include #include "common.h" #include "git.h" #include "domain.h" #include "treeview.h" #include "listview.h" #include "filelist.h" #include "revdesc.h" #include "patchview.h" #include "smartbrowse.h" #include "mainimpl.h" #include "revsview.h" RevsView::RevsView(MainImpl* mi, Git* g, bool isMain) : Domain(mi, g, isMain) { revTab = new Ui_TabRev(); revTab->setupUi(container); tab()->listViewLog->setup(this, g); tab()->textBrowserDesc->setup(this); tab()->textEditDiff->setup(this, git); tab()->fileList->setup(this, git); m()->treeView->setup(this, git); setTabLogDiffVisible(QGit::testFlag(QGit::LOG_DIFF_TAB_F)); SmartBrowse* sb = new SmartBrowse(this); // restore geometry QVector v; v << tab()->horizontalSplitter << tab()->verticalSplitter; QGit::restoreGeometrySetting(QGit::REV_GEOM_KEY, NULL, &v); connect(m(), SIGNAL(typeWriterFontChanged()), tab()->textEditDiff, SLOT(typeWriterFontChanged())); connect(m(), SIGNAL(flagChanged(uint)), sb, SLOT(flagChanged(uint))); connect(git, SIGNAL(newRevsAdded(const FileHistory*, const QVector&)), this, SLOT(on_newRevsAdded(const FileHistory*, const QVector&))); connect(git, SIGNAL(loadCompleted(const FileHistory*, const QString&)), this, SLOT(on_loadCompleted(const FileHistory*, const QString&))); connect(m(), SIGNAL(changeFont(const QFont&)), tab()->listViewLog, SLOT(on_changeFont(const QFont&))); connect(m(), SIGNAL(updateRevDesc()), this, SLOT(on_updateRevDesc())); connect(tab()->listViewLog, SIGNAL(lanesContextMenuRequested(const QStringList&, const QStringList&)), this, SLOT(on_lanesContextMenuRequested (const QStringList&, const QStringList&))); connect(tab()->listViewLog, SIGNAL(revisionsDragged(const QStringList&)), m(), SLOT(revisionsDragged(const QStringList&))); connect(tab()->listViewLog, SIGNAL(revisionsDropped(const QStringList&)), m(), SLOT(revisionsDropped(const QStringList&))); connect(tab()->listViewLog, SIGNAL(contextMenu(const QString&, int)), this, SLOT(on_contextMenu(const QString&, int))); connect(m()->treeView, SIGNAL(contextMenu(const QString&, int)), this, SLOT(on_contextMenu(const QString&, int))); connect(tab()->fileList, SIGNAL(contextMenu(const QString&, int)), this, SLOT(on_contextMenu(const QString&, int))); connect(m(), SIGNAL(changeFont(const QFont&)), tab()->fileList, SLOT(on_changeFont(const QFont&))); connect(m(), SIGNAL(highlightPatch(const QString&, bool)), tab()->textEditDiff, SLOT(on_highlightPatch(const QString&, bool))); } RevsView::~RevsView() { if (!parent()) return; QVector v; v << tab()->horizontalSplitter << tab()->verticalSplitter; QGit::saveGeometrySetting(QGit::REV_GEOM_KEY, NULL, &v); // manually delete before container is removed in Domain // d'tor to avoid a crash due to spurious events in // SmartBrowse::eventFilter() delete tab()->textBrowserDesc; delete tab()->textEditDiff; delete linkedPatchView; delete revTab; } void RevsView::clear(bool complete) { Domain::clear(complete); tab()->textBrowserDesc->clear(); tab()->textEditDiff->clear(); tab()->fileList->clear(); m()->treeView->clear(); updateLineEditSHA(true); if (linkedPatchView) linkedPatchView->clear(); } void RevsView::setEnabled(bool b) { container->setEnabled(b); if (linkedPatchView) linkedPatchView->tabPage()->setEnabled(b); } void RevsView::toggleDiffView() { QStackedWidget* s = tab()->stackedPanes; QTabWidget* t = tab()->tabLogDiff; bool isTabPage = (s->currentIndex() == 0); int idx = (isTabPage ? t->currentIndex() : s->currentIndex()); bool old = container->updatesEnabled(); container->setUpdatesEnabled(false); if (isTabPage) t->setCurrentIndex(1 - idx); else s->setCurrentIndex(3 - idx); container->setUpdatesEnabled(old); } void RevsView::setTabLogDiffVisible(bool b) { QStackedWidget* s = tab()->stackedPanes; QTabWidget* t = tab()->tabLogDiff; bool isTabPage = (s->currentIndex() == 0); int idx = (isTabPage ? t->currentIndex() : s->currentIndex()); container->setUpdatesEnabled(false); if (b && !isTabPage) { t->addTab(tab()->textBrowserDesc, "Log"); t->addTab(tab()->textEditDiff, "Diff"); t->setCurrentIndex(idx - 1); s->setCurrentIndex(0); } if (!b && isTabPage) { s->addWidget(tab()->textBrowserDesc); s->addWidget(tab()->textEditDiff); // manually remove the two remaining empty pages t->removeTab(0); t->removeTab(0); s->setCurrentIndex(idx + 1); } container->setUpdatesEnabled(true); } void RevsView::viewPatch(bool newTab) { if (!newTab && linkedPatchView) { m()->tabWdg->setCurrentWidget(linkedPatchView->tabPage()); return; } PatchView* pv = new PatchView(m(), git); m()->tabWdg->addTab(pv->tabPage(), "&Patch"); m()->tabWdg->setCurrentWidget(pv->tabPage()); if (!newTab) { // linkedPatchView == NULL linkedPatchView = pv; linkDomain(linkedPatchView); connect(m(), SIGNAL(highlightPatch(const QString&, bool)), pv->tab()->textEditDiff, SLOT(on_highlightPatch(const QString&, bool))); connect(pv->tab()->fileList, SIGNAL(itemDoubleClicked(QListWidgetItem*)), m(), SLOT(fileList_itemDoubleClicked(QListWidgetItem*))); } connect(m(), SIGNAL(updateRevDesc()), pv, SLOT(on_updateRevDesc())); connect(m(), SIGNAL(closeAllTabs()), pv, SLOT(on_closeAllTabs())); pv->st = st; UPDATE_DM_MASTER(pv, false); } void RevsView::on_newRevsAdded(const FileHistory* fh, const QVector&) { if (!git->isMainHistory(fh) || !st.sha().isEmpty()) return; ListView* lv = tab()->listViewLog; if (lv->model()->rowCount() == 0) return; st.setSha(lv->sha(0)); st.setSelectItem(true); UPDATE(); // give feedback to user as soon as possible } void RevsView::on_loadCompleted(const FileHistory* fh, const QString& stats) { if (!git->isMainHistory(fh)) return; UPDATE(); // restore revision after a refresh QApplication::postEvent(this, new MessageEvent(stats)); } void RevsView::on_updateRevDesc() { SCRef d = m()->getRevisionDesc(st.sha()); tab()->textBrowserDesc->setHtml(d); } bool RevsView::doUpdate(bool force) { force = force || m()->lineEditSHA->text().isEmpty(); bool found = tab()->listViewLog->update(); if (!found && !st.sha().isEmpty()) { const QString tmp("Sorry, revision " + st.sha() + " has not been found in main view"); showStatusBarMessage(tmp); } else { // sha could be NULL if (st.isChanged(StateInfo::SHA) || force) { updateLineEditSHA(); on_updateRevDesc(); showStatusBarMessage(git->getRevInfo(st.sha())); if ( testFlag(QGit::MSG_ON_NEW_F) && tab()->textEditDiff->isVisible()) toggleDiffView(); } const RevFile* files = NULL; bool newFiles = false; if (st.isChanged(StateInfo::ANY & ~StateInfo::FILE_NAME) || force) { tab()->fileList->clear(); if (linkedPatchView) // give some feedback while waiting linkedPatchView->clear(); // blocking call, could be slow in case of all merge files files = git->getFiles(st.sha(), st.diffToSha(), st.allMergeFiles()); newFiles = true; tab()->textEditDiff->update(st); } // call always to allow a simple refresh tab()->fileList->update(files, newFiles); // update the tree at startup or when releasing a no-match toolbar search if (m()->treeView->isVisible() || st.sha(false).isEmpty()) m()->treeView->updateTree(); // blocking call if (st.selectItem()) { bool isDir = m()->treeView->isDir(st.fileName()); m()->updateContextActions(st.sha(), st.fileName(), isDir, found); } if (st.isChanged() || force) { // activate log or diff tab depending on file selection tab()->tabLogDiff->setCurrentIndex(st.fileName().isEmpty() ? 0 : 1); tab()->textEditDiff->centerOnFileHeader(st); } // at the end update diffs that is the slowest and must be // run after update of file list for 'diff to sha' to work if (linkedPatchView) { linkedPatchView->st = st; UPDATE_DM_MASTER(linkedPatchView, force); // async call } } return (found || st.sha().isEmpty()); } void RevsView::updateLineEditSHA(bool clear) { QLineEdit* l = m()->lineEditSHA; if (clear) l->setText(""); // clears history else if (l->text() != st.sha()) { if (l->text().isEmpty()) l->setText(st.sha()); // first rev clears history else { // setText() clears undo/redo history so // we use clear() + insert() instead l->clear(); l->insert(st.sha()); } } m()->ActBack->setEnabled(l->isUndoAvailable()); m()->ActForward->setEnabled(l->isRedoAvailable()); } void RevsView::on_lanesContextMenuRequested(SCList parents, SCList children) { QMenu contextMenu; FOREACH_SL (it, children) contextMenu.addAction("Child: " + git->getShortLog(*it)); FOREACH_SL (it, parents) { QString log(git->getShortLog(*it)); contextMenu.addAction("Parent: " + (log.isEmpty() ? *it : log)); } QAction* act = contextMenu.exec(QCursor::pos()); // modal exec if (!act) return; int cc = children.count(); int id = contextMenu.actions().indexOf(act); SCRef target(id < cc ? children[id] : parents[id - cc]); st.setSha(target); UPDATE(); } qgit-2.7/src/revsview.h000066400000000000000000000021411305655150700151450ustar00rootroot00000000000000/* Author: Marco Costalba (C) 2005-2007 Copyright: See COPYING file that comes with this distribution */ #ifndef REVSVIEW_H #define REVSVIEW_H #include #include "ui_revsview.h" // needed by moc_* file to understand tab() function #include "common.h" #include "domain.h" class MainImpl; class Git; class FileHistory; class PatchView; class RevsView : public Domain { Q_OBJECT public: RevsView(MainImpl* parent, Git* git, bool isMain = false); ~RevsView(); void clear(bool complete); void viewPatch(bool newTab); void setEnabled(bool b); void setTabLogDiffVisible(bool); Ui_TabRev* tab() { return revTab; } public slots: void toggleDiffView(); private slots: void on_newRevsAdded(const FileHistory*, const QVector&); void on_loadCompleted(const FileHistory*, const QString& stats); void on_lanesContextMenuRequested(const QStringList&, const QStringList&); void on_updateRevDesc(); protected: virtual bool doUpdate(bool force); private: friend class MainImpl; void updateLineEditSHA(bool clear = false); Ui_TabRev* revTab; QPointer linkedPatchView; }; #endif qgit-2.7/src/revsview.ui000066400000000000000000000120531305655150700153360ustar00rootroot00000000000000 TabRev 0 0 693 523 Rev list 0 0 Qt::Vertical Qt::CustomContextMenu true true QAbstractItemView::ExtendedSelection false true false true Qt::Horizontal 0 0 0 346 249 QTabWidget::East QTabWidget::Triangular 0 0 0 317 243 Log 0 0 QTextEdit::WidgetWidth 0 0 317 243 Diff 0 0 test false QTextEdit::NoWrap true Qt::CustomContextMenu ListView QTreeView
listview.h
RevDesc QTextBrowser
revdesc.h
FileList QListWidget
filelist.h
PatchContent QTextEdit
patchcontent.h
revdesc.h
qgit-2.7/src/settings.ui000066400000000000000000001322351305655150700153310ustar00rootroot00000000000000 settingsBase 0 0 513 377 0 0 Settings :/icons/resources/configure.png:/icons/resources/configure.png false true 6 9 6 0 5 &General 6 9 6 0 QFrame::StyledPanel QFrame::Raised 6 9 6 0 Check to see commit author date as relative time from now Relati&ve time in date column Alt+V Check to view range select dialog at repository opening Show range select when opening a repositor&y Alt+Y Reopen last repository on startup Double click opens file in editor Qt::Vertical 20 21 0 4 40 16777215 Browse directories ... Typewriter (fixed width) font used by patch and file viewers true Fixed width font false lineEditTypeWriterFont Set path of the external diff tool &External diff tool false lineEditExternalDiffViewer Set path of the external editor &External editor false lineEditExternalEditor 40 16777215 Browse directories ... 40 16777215 Choose font ... Use %1 and %2 to denote files to diff. Use %1 to denote files to edit. Qt::Vertical 20 20 4 0 Select text codec to use. You need to refresh the view (F5) after a codec change Te&xt codec false comboBoxCodecs 1 0 Select text codec to use. You need to refresh the view (F5) after a codec change &Browse 6 9 0 0 QFrame::StyledPanel QFrame::Raised 0 2 4 0 Check to see tabbed revision log / diff pane Show tabbed revision msg / diff pane Check to show smart jump labels on revision description pane Show smart labels Check to see always the revision description in main view when browsing the repo. Patch content will be visible after request. Show always revision message as first Alt+Y Qt::Vertical 20 71 Patc&h QFrame::StyledPanel QFrame::Raised 0 2 4 0 Check to see patch numbers on patch series header Num&bered patches Alt+B Check to add a 'Signed-off-by:' line if not already existent &Sign off patch Alt+S Qt::Vertical 399 91 &Apply patch extra options false lineEditApplyPatchExtraOptions Extra options to pass to 'git am' 32767 &Format patch extra options false lineEditFormatPatchExtraOptions Extra options to pass to 'git format-patch' 32767 &Working directory 6 9 0 0 QFrame::StyledPanel QFrame::Raised 4 5 Check to see git status. You need this to commit changes Diff again&st working directory Alt+S Qt::Vertical 20 40 0 4 Exclude patterns file that apply only to the directory and its subdirectories Exclude patterns are read from this file one per line E&xclude per dir false lineEditExcludePerDir &Exclude file path false lineEditExcludeFile &Commit 6 9 0 0 QFrame::StyledPanel QFrame::Raised 0 2 4 3 Check to add -s option to git commit-script &Sign off commit Alt+S Check to add -v option to git commit-script &Verify commit Alt+V Use default commit message Qt::Vertical 20 21 0 4 Defined in: Author lineEditAuthor true true E-mail lineEditMail Qt::Vertical 20 21 0 4 Ms&g template Qt::PlainText false lineEditAuthor &Extra options false lineEditCommitExtraOptions Commit message template file Extra options to pass to git commit-script Gi&t config Qt::Horizontal 40 20 Git options source: Local Global 2 Parameter Value 0 0 Qt::Horizontal 181 20 O&K Alt+K false false buttonOk clicked() settingsBase close() 501 365 20 20 checkBoxNumbers toggled(bool) settingsBase checkBoxNumbers_toggled(bool) 26 47 20 20 lineEditFormatPatchExtraOptions textChanged(QString) settingsBase lineEditFormatPatchExtraOptions_textChanged(QString) 160 301 20 20 checkBoxDiffCache toggled(bool) settingsBase checkBoxDiffCache_toggled(bool) 28 49 20 20 lineEditExcludePerDir textChanged(QString) settingsBase lineEditExcludePerDir_textChanged(QString) 90 301 20 20 lineEditExcludeFile textChanged(QString) settingsBase lineEditExcludeFile_textChanged(QString) 90 277 20 20 checkBoxSign toggled(bool) settingsBase checkBoxSign_toggled(bool) 26 69 20 20 lineEditTemplate textChanged(QString) settingsBase lineEditTemplate_textChanged(QString) 91 277 20 20 lineEditCommitExtraOptions textChanged(QString) settingsBase lineEditCommitExtraOptions_textChanged(QString) 91 301 20 20 checkBoxRelativeDate toggled(bool) settingsBase checkBoxRelativeDate_toggled(bool) 33 54 20 20 checkBoxRangeSelectDialog toggled(bool) settingsBase checkBoxRangeSelectDialog_toggled(bool) 33 78 20 20 checkBoxReopenLastRepo toggled(bool) settingsBase checkBoxReopenLastRepo_toggled(bool) 33 102 20 20 checkBoxOpenInEditor toggled(bool) settingsBase checkBoxOpenInEditor_toggled(bool) 33 102 20 20 comboBoxCodecs activated(int) settingsBase comboBoxCodecs_activated(int) 112 320 20 20 checkBoxCommitVerify toggled(bool) settingsBase checkBoxCommitVerify_toggled(bool) 28 71 20 20 checkBoxCommitSign toggled(bool) settingsBase checkBoxCommitSign_toggled(bool) 28 49 20 20 checkBoxCommitUseDefMsg toggled(bool) settingsBase checkBoxCommitUseDefMsg_toggled(bool) 47 101 20 20 checkBoxCommitSign toggled(bool) settingsBase checkBoxCommitSign_toggled(bool) 28 49 20 20 lineEditExternalDiffViewer textChanged(QString) settingsBase lineEditExternalDiffViewer_textChanged(QString) 175 227 20 20 lineEditExternalEditor textChanged(QString) settingsBase lineEditExternalEditor_textChanged(QString) 175 227 20 20 pushButtonExtDiff clicked() settingsBase pushButtonExtDiff_clicked() 487 230 20 20 pushButtonFont clicked() settingsBase pushButtonFont_clicked() 487 201 20 20 comboBoxUserSrc activated(int) settingsBase comboBoxUserSrc_activated(int) 440 206 256 195 checkBoxMsgOnNewSHA toggled(bool) settingsBase checkBoxMsgOnNewSHA_toggled(bool) 45 91 225 194 checkBoxLogDiffTab toggled(bool) settingsBase checkBoxLogDiffTab_toggled(bool) 45 47 225 194 checkBoxSmartLabels toggled(bool) settingsBase checkBoxSmartLabels_toggled(bool) 45 69 225 194 lineEditApplyPatchExtraOptions textChanged(QString) settingsBase lineEditApplyPatchExtraOptions_textChanged(QString) 160 275 221 171 comboBoxGitConfigSource activated(int) settingsBase comboBoxGitConfigSource_activated(int) 463 54 159 89 treeWidgetGitConfig itemChanged(QTreeWidgetItem*,int) settingsBase treeWidgetGitConfig_itemChanged(QTreeWidgetItem*, int) 236 172 147 359 qgit-2.7/src/settingsimpl.cpp000066400000000000000000000241221305655150700163530ustar00rootroot00000000000000/* Description: settings dialog Author: Marco Costalba (C) 2005-2007 Copyright: See COPYING file that comes with this distribution */ #include #include #include #include #include "common.h" #include "git.h" #include "settingsimpl.h" /* By default, there are two entries in the search path: 1. SYSCONF - where SYSCONF is a directory specified when configuring Qt; by default it is INSTALL/etc/settings. 2. $HOME/.qt/ - where $HOME is the user's home directory. */ static const char* en[] = { "Latin1", "Big5 -- Chinese", "EUC-JP -- Japanese", "EUC-KR -- Korean", "GB18030 -- Chinese", "ISO-2022-JP -- Japanese", "Shift_JIS -- Japanese", "UTF-8 -- Unicode, 8-bit", "KOI8-R -- Russian", "KOI8-U -- Ukrainian", "ISO-8859-1 -- Western", "ISO-8859-2 -- Central European", "ISO-8859-3 -- Central European", "ISO-8859-4 -- Baltic", "ISO-8859-5 -- Cyrillic", "ISO-8859-6 -- Arabic", "ISO-8859-7 -- Greek", "ISO-8859-8 -- Hebrew, visually ordered", "ISO-8859-8-i -- Hebrew, logically ordered", "ISO-8859-9 -- Turkish", "ISO-8859-10", "ISO-8859-13", "ISO-8859-14", "ISO-8859-15 -- Western", "windows-1250 -- Central European", "windows-1251 -- Cyrillic", "windows-1252 -- Western", "windows-1253 -- Greek", "windows-1254 -- Turkish", "windows-1255 -- Hebrew", "windows-1256 -- Arabic", "windows-1257 -- Baltic", "windows-1258", 0 }; using namespace QGit; SettingsImpl::SettingsImpl(QWidget* p, Git* g, int defTab) : QDialog(p), git(g) { setupUi(this); int f = flags(FLAGS_KEY); checkBoxDiffCache->setChecked(f & DIFF_INDEX_F); checkBoxNumbers->setChecked(f & NUMBERS_F); checkBoxSign->setChecked(f & SIGN_PATCH_F); checkBoxCommitSign->setChecked(f & SIGN_CMT_F); checkBoxCommitVerify->setChecked(f & VERIFY_CMT_F); checkBoxCommitUseDefMsg->setChecked(f & USE_CMT_MSG_F); checkBoxRangeSelectDialog->setChecked(f & RANGE_SELECT_F); checkBoxReopenLastRepo->setChecked(f & REOPEN_REPO_F); checkBoxOpenInEditor->setChecked(f & OPEN_IN_EDITOR_F); checkBoxRelativeDate->setChecked(f & REL_DATE_F); checkBoxLogDiffTab->setChecked(f & LOG_DIFF_TAB_F); checkBoxSmartLabels->setChecked(f & SMART_LBL_F); checkBoxMsgOnNewSHA->setChecked(f & MSG_ON_NEW_F); QSettings set; SCRef APOpt(set.value(AM_P_OPT_KEY).toString()); SCRef FPOpt(set.value(FMT_P_OPT_KEY).toString()); SCRef extDiff(set.value(EXT_DIFF_KEY, EXT_DIFF_DEF).toString()); SCRef extEditor(set.value(EXT_EDITOR_KEY, EXT_EDITOR_DEF).toString()); SCRef exFile(set.value(EX_KEY, EX_DEF).toString()); SCRef exPDir(set.value(EX_PER_DIR_KEY, EX_PER_DIR_DEF).toString()); SCRef tmplt(set.value(CMT_TEMPL_KEY, CMT_TEMPL_DEF).toString()); SCRef CMArgs(set.value(CMT_ARGS_KEY).toString()); lineEditApplyPatchExtraOptions->setText(APOpt); lineEditFormatPatchExtraOptions->setText(FPOpt); lineEditExternalDiffViewer->setText(extDiff); lineEditExternalEditor->setText(extEditor); lineEditExcludeFile->setText(exFile); lineEditExcludePerDir->setText(exPDir); lineEditTemplate->setText(tmplt); lineEditCommitExtraOptions->setText(CMArgs); lineEditTypeWriterFont->setText(TYPE_WRITER_FONT.toString()); lineEditTypeWriterFont->setCursorPosition(0); // font description could be long setupCodecsCombo(); checkBoxDiffCache_toggled(checkBoxDiffCache->isChecked()); tabDialog->setCurrentIndex(defTab); userInfo(); comboBoxGitConfigSource_activated(0); } void SettingsImpl::userInfo() { /* QGit::userInfo() returns a QStringList formed by triples (defined in, user, email) */ git->userInfo(_uInfo); if (_uInfo.count() % 3 != 0) { dbs("ASSERT in SettingsImpl::userInfo(), bad info returned"); return; } bool found = false; int idx = 0; FOREACH_SL(it, _uInfo) { comboBoxUserSrc->addItem(*it); ++it; if (!found && !(*it).isEmpty()) found = true; if (!found) idx++; ++it; } if (!found) idx = 0; comboBoxUserSrc->setCurrentIndex(idx); comboBoxUserSrc_activated(idx); } void SettingsImpl::addConfigOption(QTreeWidgetItem* parent, QStringList paths, const QString& value) { if (paths.isEmpty()) { parent->setText(1, value); return; } QString name(paths.first()); paths.removeFirst(); // Options list is already ordered if (parent->childCount() == 0 || name != parent->child(0)->text(0)) parent->addChild(new QTreeWidgetItem(parent, QStringList(name))); addConfigOption(parent->child(parent->childCount() - 1), paths, value); } void SettingsImpl::readGitConfig(const QString& source) { populatingGitConfig = true; treeWidgetGitConfig->clear(); QStringList options(git->getGitConfigList(source == "Global")); options.sort(); FOREACH_SL(it, options) { QStringList paths = it->split("=").at(0).split("."); QString value = it->split("=").at(1); if (paths.isEmpty() || value.isEmpty()) { dbp("SettingsImpl::readGitConfig Unable to parse line %1", *it); continue; } QString name(paths.first()); paths.removeFirst(); QList items = treeWidgetGitConfig->findItems(name, Qt::MatchExactly); QTreeWidgetItem* item; if (items.isEmpty()) item = new QTreeWidgetItem(treeWidgetGitConfig, QStringList(name)); else item = items.first(); addConfigOption(item, paths, value); } populatingGitConfig = false; } void SettingsImpl::treeWidgetGitConfig_itemChanged(QTreeWidgetItem* item, int i) { if (populatingGitConfig) return; dbs(item->text(0));dbs(item->text(1));dbp("column %1", i); } void SettingsImpl::comboBoxUserSrc_activated(int i) { lineEditAuthor->setText(_uInfo[i * 3 + 1]); lineEditMail->setText(_uInfo[i * 3 + 2]); } void SettingsImpl::comboBoxGitConfigSource_activated(int) { readGitConfig(comboBoxGitConfigSource->currentText()); } void SettingsImpl::writeSetting(const QString& key, const QVariant& value) { QSettings settings; settings.setValue(key, value); } void SettingsImpl::setupCodecList(QStringList& list) { int i = 0; while (en[i] != 0) list.append(QString::fromLatin1(en[i++])); } void SettingsImpl::setupCodecsCombo() { const QString localCodec(QTextCodec::codecForLocale()->name()); QStringList codecs; codecs.append(QString("Local Codec (" + localCodec + ")")); setupCodecList(codecs); comboBoxCodecs->insertItems(0, codecs); bool isGitArchive; QTextCodec* tc = git->getTextCodec(&isGitArchive); if (!isGitArchive) { comboBoxCodecs->setEnabled(false); return; } const QString curCodec(tc != 0 ? tc->name() : "Latin1"); QRegExp re("*" + curCodec + "*", Qt::CaseInsensitive, QRegExp::Wildcard); int idx = codecs.indexOf(re); if (idx == -1) { dbp("ASSERT: codec <%1> not available, using local codec", curCodec); idx = 0; } comboBoxCodecs->setCurrentIndex(idx); if (idx == 0) // signal activated() will not fire in this case comboBoxCodecs_activated(0); } void SettingsImpl::comboBoxCodecs_activated(int idx) { QString codecName(QTextCodec::codecForLocale()->name()); if (idx != 0) codecName = comboBoxCodecs->currentText().section(" --", 0, 0); git->setTextCodec(QTextCodec::codecForName(codecName.toLatin1())); } void SettingsImpl::pushButtonExtDiff_clicked() { QString extDiffName(QFileDialog::getOpenFileName(this, "Select the patch viewer")); if (!extDiffName.isEmpty()) lineEditExternalDiffViewer->setText(extDiffName); } void SettingsImpl::pushButtonExtEditor_clicked() { QString extEditorName(QFileDialog::getOpenFileName(this, "Select the external editor")); if (!extEditorName.isEmpty()) lineEditExternalEditor->setText(extEditorName); } void SettingsImpl::pushButtonFont_clicked() { bool ok; QFont fnt = QFontDialog::getFont(&ok, TYPE_WRITER_FONT, this); if (ok && TYPE_WRITER_FONT != fnt) { TYPE_WRITER_FONT = fnt; lineEditTypeWriterFont->setText(fnt.toString()); lineEditTypeWriterFont->setCursorPosition(0); writeSetting(TYPWRT_FNT_KEY, fnt.toString()); emit typeWriterFontChanged(); } } void SettingsImpl::changeFlag(uint f, bool b) { setFlag(f, b); emit flagChanged(f); } void SettingsImpl::checkBoxDiffCache_toggled(bool b) { lineEditExcludeFile->setEnabled(b); lineEditExcludePerDir->setEnabled(b); changeFlag(DIFF_INDEX_F, b); } void SettingsImpl::checkBoxNumbers_toggled(bool b) { changeFlag(NUMBERS_F, b); } void SettingsImpl::checkBoxSign_toggled(bool b) { changeFlag(SIGN_PATCH_F, b); } void SettingsImpl::checkBoxRangeSelectDialog_toggled(bool b) { changeFlag(RANGE_SELECT_F, b); } void SettingsImpl::checkBoxReopenLastRepo_toggled(bool b) { changeFlag(REOPEN_REPO_F, b); } void SettingsImpl::checkBoxOpenInEditor_toggled(bool b) { changeFlag(OPEN_IN_EDITOR_F, b); } void SettingsImpl::checkBoxRelativeDate_toggled(bool b) { changeFlag(REL_DATE_F, b); } void SettingsImpl::checkBoxLogDiffTab_toggled(bool b) { changeFlag(LOG_DIFF_TAB_F, b); } void SettingsImpl::checkBoxSmartLabels_toggled(bool b) { changeFlag(SMART_LBL_F, b); } void SettingsImpl::checkBoxMsgOnNewSHA_toggled(bool b) { changeFlag(MSG_ON_NEW_F, b); } void SettingsImpl::checkBoxCommitSign_toggled(bool b) { changeFlag(SIGN_CMT_F, b); } void SettingsImpl::checkBoxCommitVerify_toggled(bool b) { changeFlag(VERIFY_CMT_F, b); } void SettingsImpl::checkBoxCommitUseDefMsg_toggled(bool b) { changeFlag(USE_CMT_MSG_F, b); } void SettingsImpl::lineEditExternalDiffViewer_textChanged(const QString& s) { writeSetting(EXT_DIFF_KEY, s); } void SettingsImpl::lineEditExternalEditor_textChanged(const QString& s) { writeSetting(EXT_EDITOR_KEY, s); } void SettingsImpl::lineEditApplyPatchExtraOptions_textChanged(const QString& s) { writeSetting(AM_P_OPT_KEY, s); } void SettingsImpl::lineEditFormatPatchExtraOptions_textChanged(const QString& s) { writeSetting(FMT_P_OPT_KEY, s); } void SettingsImpl::lineEditExcludeFile_textChanged(const QString& s) { writeSetting(EX_KEY, s); } void SettingsImpl::lineEditExcludePerDir_textChanged(const QString& s) { writeSetting(EX_PER_DIR_KEY, s); } void SettingsImpl::lineEditTemplate_textChanged(const QString& s) { writeSetting(CMT_TEMPL_KEY, s); } void SettingsImpl::lineEditCommitExtraOptions_textChanged(const QString& s) { writeSetting(CMT_ARGS_KEY, s); } qgit-2.7/src/settingsimpl.h000066400000000000000000000041761305655150700160270ustar00rootroot00000000000000/* Author: Marco Costalba (C) 2005-2007 Copyright: See COPYING file that comes with this distribution */ #ifndef SETTINGSIMPL_H #define SETTINGSIMPL_H #include "ui_settings.h" class QVariant; class Git; class SettingsImpl: public QDialog, public Ui_settingsBase { Q_OBJECT public: SettingsImpl(QWidget* parent, Git* git, int defTab = 0); signals: void typeWriterFontChanged(); void flagChanged(uint); protected slots: void checkBoxNumbers_toggled(bool b); void checkBoxSign_toggled(bool b); void checkBoxRangeSelectDialog_toggled(bool b); void checkBoxReopenLastRepo_toggled(bool b); void checkBoxOpenInEditor_toggled(bool b); void checkBoxRelativeDate_toggled(bool b); void checkBoxLogDiffTab_toggled(bool b); void checkBoxSmartLabels_toggled(bool b); void checkBoxMsgOnNewSHA_toggled(bool b); void checkBoxDiffCache_toggled(bool b); void checkBoxCommitSign_toggled(bool b); void checkBoxCommitVerify_toggled(bool b); void checkBoxCommitUseDefMsg_toggled(bool b); void lineEditExternalDiffViewer_textChanged(const QString& s); void lineEditExternalEditor_textChanged(const QString& s); void lineEditApplyPatchExtraOptions_textChanged(const QString& s); void lineEditFormatPatchExtraOptions_textChanged(const QString& s); void lineEditExcludeFile_textChanged(const QString& s); void lineEditExcludePerDir_textChanged(const QString& s); void lineEditTemplate_textChanged(const QString& s); void lineEditCommitExtraOptions_textChanged(const QString& s); void comboBoxCodecs_activated(int i); void comboBoxUserSrc_activated(int i); void comboBoxGitConfigSource_activated(int i); void treeWidgetGitConfig_itemChanged(QTreeWidgetItem*, int); void pushButtonExtDiff_clicked(); void pushButtonExtEditor_clicked(); void pushButtonFont_clicked(); private: void writeSetting(const QString& key, const QVariant& value); void addConfigOption(QTreeWidgetItem* parent, QStringList paths, const QString& value); void setupCodecList(QStringList& list); void setupCodecsCombo(); void readGitConfig(const QString& source); void userInfo(); void changeFlag(uint f, bool b); Git* git; QStringList _uInfo; bool populatingGitConfig; }; #endif qgit-2.7/src/smartbrowse.cpp000066400000000000000000000204251305655150700162030ustar00rootroot00000000000000/* Author: Marco Costalba (C) 2005-2007 Copyright: See COPYING file that comes with this distribution */ #include #include #include #include #include #include "revsview.h" #include "smartbrowse.h" #define GO_UP 1 #define GO_DOWN 2 #define GO_LOG 3 #define GO_DIFF 4 #define AT_TOP 1 #define AT_BTM 2 SmartLabel::SmartLabel(const QString& text, QWidget* par) : QLabel(text, par) { this->setStyleSheet("SmartLabel { border: 1px solid LightGray;" "padding: 0px 2px 2px 2px; }"); } void SmartLabel::paintEvent(QPaintEvent* event) { // our QPainter must be destroyed before QLabel's paintEvent is called { QPainter painter(this); QColor backgroundColor = Qt::white; // give label a semi-transparent background backgroundColor.setAlpha(200); painter.fillRect(this->rect(), backgroundColor); } // let QLabel do the rest QLabel::paintEvent(event); } void SmartLabel::contextMenuEvent(QContextMenuEvent* e) { if (text().count("href=") != 2) return; QMenu* menu = new QMenu(this); menu->addAction("Switch links", this, SLOT(switchLinks())); menu->exec(e->globalPos()); delete menu; } void SmartLabel::switchLinks() { QString t(text()); QString link1(t.section("", 0, 0)); QString link2(t.section("", 0, 0)); t.replace(link1, "%1").replace(link2, "%2"); setText(t.arg(link2, link1)); adjustSize(); } SmartBrowse::SmartBrowse(RevsView* par) : QObject(par) { rv = par; wheelCnt = 0; lablesEnabled = QGit::testFlag(QGit::SMART_LBL_F); QString txt("

%2 %3

"); QString link("
%2"); QString linkUp(link.arg(QString::number(GO_UP), "Up")); QString linkDown(link.arg(QString::number(GO_DOWN), "Down")); QString linkLog(link.arg(QString::number(GO_LOG), "Log")); QString linkDiff(link.arg(QString::number(GO_DIFF), "Diff")); QTextEdit* log = static_cast(rv->tab()->textBrowserDesc); QTextEdit* diff = static_cast(rv->tab()->textEditDiff); logTopLbl = new SmartLabel(txt.arg("1uparrow.png", linkUp, ""), log); logBottomLbl = new SmartLabel(txt.arg("1downarrow.png", linkDiff, linkDown), log); diffTopLbl = new SmartLabel(txt.arg("1uparrow.png", linkLog, linkUp), diff); diffBottomLbl = new SmartLabel(txt.arg("1downarrow.png", linkUp, linkDown), diff); diffTopLbl->setFont(qApp->font()); // override parent's font to diffBottomLbl->setFont(qApp->font()); // avoid QGit::TYPE_WRITER_FONT setVisible(false); log->installEventFilter(this); diff->installEventFilter(this); QScrollBar* vsbLog = log->verticalScrollBar(); QScrollBar* vsbDiff = diff->verticalScrollBar(); vsbLog->installEventFilter(this); vsbDiff->installEventFilter(this); log->horizontalScrollBar()->installEventFilter(this); diff->horizontalScrollBar()->installEventFilter(this); connect(vsbLog, SIGNAL(valueChanged(int)), this, SLOT(updateVisibility())); connect(vsbDiff, SIGNAL(valueChanged(int)), this, SLOT(updateVisibility())); connect(logTopLbl, SIGNAL(linkActivated(const QString&)), this, SLOT(linkActivated(const QString&))); connect(logBottomLbl, SIGNAL(linkActivated(const QString&)), this, SLOT(linkActivated(const QString&))); connect(diffTopLbl, SIGNAL(linkActivated(const QString&)), this, SLOT(linkActivated(const QString&))); connect(diffBottomLbl, SIGNAL(linkActivated(const QString&)), this, SLOT(linkActivated(const QString&))); } void SmartBrowse::setVisible(bool b) { b = b && lablesEnabled; logTopLbl->setVisible(b); logBottomLbl->setVisible(b); diffTopLbl->setVisible(b); diffBottomLbl->setVisible(b); } QTextEdit* SmartBrowse::curTextEdit(bool* isDiff) { QTextEdit* log = static_cast(rv->tab()->textBrowserDesc); QTextEdit* diff = static_cast(rv->tab()->textEditDiff); if (isDiff) *isDiff = diff->isVisible(); if (!diff->isVisible() && !log->isVisible()) return NULL; return (diff->isVisible() ? diff : log); } int SmartBrowse::visibilityFlags(bool* isDiff) { static int MIN = 5; QTextEdit* te = curTextEdit(isDiff); if (!te) return 0; QScrollBar* vsb = te->verticalScrollBar(); bool v = lablesEnabled && te->isEnabled(); bool top = v && (!vsb->isVisible() || (vsb->value() - vsb->minimum() < MIN)); bool btm = v && (!vsb->isVisible() || (vsb->maximum() - vsb->value() < MIN)); return AT_TOP * top + AT_BTM * btm; } void SmartBrowse::updateVisibility() { bool isDiff; int flags = visibilityFlags(&isDiff); if (isDiff) { diffTopLbl->setVisible(flags & AT_TOP); diffBottomLbl->setVisible(flags & AT_BTM); } else { logTopLbl->setVisible(flags & AT_TOP); logBottomLbl->setVisible(flags & AT_BTM); } } void SmartBrowse::flagChanged(uint flag) { if (flag == QGit::SMART_LBL_F && curTextEdit()) { lablesEnabled = QGit::testFlag(QGit::SMART_LBL_F); setVisible(curTextEdit()->isEnabled()); updatePosition(); } if (flag == QGit::LOG_DIFF_TAB_F) rv->setTabLogDiffVisible(QGit::testFlag(QGit::LOG_DIFF_TAB_F)); } void SmartBrowse::linkActivated(const QString& text) { int key = text.toInt(); switch (key) { case GO_LOG: case GO_DIFF: rv->toggleDiffView(); break; case GO_UP: rv->tab()->listViewLog->on_keyUp(); break; case GO_DOWN: rv->tab()->listViewLog->on_keyDown(); break; default: dbp("ASSERT in SmartBrowse::linkActivated, key %1 not known", text); } } bool SmartBrowse::eventFilter(QObject *obj, QEvent *event) { if (!lablesEnabled) return QObject::eventFilter(obj, event); QTextEdit* te = dynamic_cast(obj); QScrollBar* sb = dynamic_cast(obj); QEvent::Type t = event->type(); if (te && t == QEvent::Resize) updatePosition(); if (sb && (t == QEvent::Show || t == QEvent::Hide)) updatePosition(); if (te && t == QEvent::EnabledChange) { setVisible(te->isEnabled()); updatePosition(); } if (sb && t == QEvent::Wheel && sb->orientation() == Qt::Vertical) { QWheelEvent* we = static_cast(event); if (wheelRolled(we->delta(), visibilityFlags())) return true; // filter event out } return QObject::eventFilter(obj, event); } void SmartBrowse::updatePosition() { QTextEdit* te = curTextEdit(); if (!te) return; QScrollBar* vb = te->verticalScrollBar(); QScrollBar* hb = te->horizontalScrollBar(); int w = te->width() - vb->width() * vb->isVisible(); int h = te->height() - hb->height() * hb->isVisible(); logTopLbl->move(w - logTopLbl->width() - 10, 10); diffTopLbl->move(w - diffTopLbl->width() - 10, 10); logBottomLbl->move(w - logBottomLbl->width() - 10, h - logBottomLbl->height() - 10); diffBottomLbl->move(w - diffBottomLbl->width() - 10, h - diffBottomLbl->height() - 10); updateVisibility(); // we are called also when user toggle view manually, // so reset wheel counters to be sure we don't have alias scrollTimer.restart(); wheelCnt = 0; } bool SmartBrowse::wheelRolled(int delta, int flags) { bool justSwitched = (switchTimer.isValid() && switchTimer.elapsed() < 400); if (justSwitched) switchTimer.restart(); bool scrolling = (scrollTimer.isValid() && scrollTimer.elapsed() < 400); bool directionChanged = (wheelCnt * delta < 0); // we are called before the scroll bar is updated, so we need // to take in account roll direction to avoid false positives bool scrollingOut = ( ((flags & AT_TOP) && (delta > 0)) ||((flags & AT_BTM) && (delta < 0))); // a scroll action have to start when in range // but can continue also when goes out of range if (!scrollingOut || scrolling) scrollTimer.restart(); if (!scrollingOut || justSwitched) return justSwitched; // filter wheels events just after a switch // we want a quick rolling action to be considered valid bool tooSlow = (timeoutTimer.isValid() && timeoutTimer.elapsed() > 300); timeoutTimer.restart(); if (directionChanged || scrolling || tooSlow) wheelCnt = 0; // ok, we would be ready to switch, but we want to add some inertia wheelCnt += (delta > 0 ? 1 : -1); if (wheelCnt * wheelCnt < 9) return false; QLabel* l; if (wheelCnt > 0) l = logTopLbl->isVisible() ? logTopLbl : diffTopLbl; else l = logBottomLbl->isVisible() ? logBottomLbl : diffBottomLbl; wheelCnt = 0; switchTimer.restart(); linkActivated(l->text().section("href=", 1).section("\"", 1, 1)); return false; } qgit-2.7/src/smartbrowse.h000066400000000000000000000021231305655150700156430ustar00rootroot00000000000000/* Author: Marco Costalba (C) 2005-2007 Copyright: See COPYING file that comes with this distribution */ #ifndef SMARTBROWSE_H #define SMARTBROWSE_H #include #include #include "revsview.h" class SmartLabel : public QLabel { Q_OBJECT public: SmartLabel(const QString& text, QWidget* par); void paintEvent(QPaintEvent* event); protected: virtual void contextMenuEvent(QContextMenuEvent* e); private slots: void switchLinks(); }; class SmartBrowse : public QObject { Q_OBJECT public: SmartBrowse(RevsView* par); protected: bool eventFilter(QObject *obj, QEvent *event); public slots: void updateVisibility(); void linkActivated(const QString&); void flagChanged(uint); private: QTextEdit* curTextEdit(bool* isDiff = NULL); void setVisible(bool b); void updatePosition(); int visibilityFlags(bool* isDiff = NULL); bool wheelRolled(int delta, int flags); RevsView* rv; SmartLabel* logTopLbl; SmartLabel* logBottomLbl; SmartLabel* diffTopLbl; SmartLabel* diffBottomLbl; QTime scrollTimer, switchTimer, timeoutTimer; int wheelCnt; bool lablesEnabled; }; #endif qgit-2.7/src/src.pro000066400000000000000000000074311305655150700144420ustar00rootroot00000000000000# Under Windows launch script start_qgit.bat needs the # value GIT_EXEC_DIR to be set to the git bin directory GIT_EXEC_DIR = "$$(ProgramFiles)\\Git\\bin" # Under Windows uncomment following line to enable console messages #CONFIG += ENABLE_CONSOLE_MSG # check for Qt >= 4.3.0 CUR_QT = $$[QT_VERSION] # WARNING greaterThan is an undocumented function !greaterThan(CUR_QT, 4.3) { error("Sorry I need Qt 4.3.0 or later, you seem to have Qt $$CUR_QT instead") } # check for g++ compiler contains(QMAKE_CC,.*g\\+\\+.*) { CONFIG += HAVE_GCC } contains(QMAKE_CC,.*gcc.*) { CONFIG += HAVE_GCC } # General stuff TEMPLATE = app CONFIG += qt warn_on exceptions debug_and_release CONFIG += uic INCLUDEPATH += ../src MAKEFILE = qmake RESOURCES += icons.qrc # Qt5 needs "widgets" greaterThan(QT_MAJOR_VERSION, 4): QT += widgets # Platform dependent stuff win32 { TARGET = qgit target.path = $$GIT_EXEC_DIR CONFIG += windows embed_manifest_exe RC_FILE = app_icon.rc } unix { TARGET = qgit target.path = $$[QT_INSTALL_BINS] CONFIG += x11 } macx { TARGET = qgit target.path = ~/bin #CONFIG += x86 ppc RC_FILE = resources/qgit.icns } HAVE_GCC { QMAKE_CXXFLAGS_RELEASE += -s -O2 -Wno-non-virtual-dtor -Wno-long-long -pedantic -Wconversion QMAKE_CXXFLAGS_DEBUG += -g3 -ggdb -O0 -Wno-non-virtual-dtor -Wno-long-long -pedantic -Wconversion } ENABLE_CONSOLE_MSG { CONFIG -= windows CONFIG += console } INSTALLS += target # Directories DESTDIR = ../bin UI_DIR = $$BUILD_DIR MOC_DIR = $$BUILD_DIR RCC_DIR = $$BUILD_DIR OBJECTS_DIR = $$BUILD_DIR # project files FORMS += commit.ui console.ui customaction.ui fileview.ui help.ui \ mainview.ui patchview.ui rangeselect.ui revsview.ui settings.ui HEADERS += annotate.h cache.h commitimpl.h common.h config.h consoleimpl.h \ customactionimpl.h dataloader.h domain.h exceptionmanager.h \ filecontent.h filelist.h fileview.h git.h help.h inputdialog.h lanes.h \ listview.h mainimpl.h myprocess.h patchcontent.h patchview.h \ rangeselectimpl.h revdesc.h revsview.h settingsimpl.h \ smartbrowse.h treeview.h \ FileHistory.h SOURCES += annotate.cpp cache.cpp commitimpl.cpp consoleimpl.cpp \ customactionimpl.cpp dataloader.cpp domain.cpp exceptionmanager.cpp \ filecontent.cpp filelist.cpp fileview.cpp git.cpp inputdialog.cpp \ lanes.cpp listview.cpp mainimpl.cpp myprocess.cpp namespace_def.cpp \ patchcontent.cpp patchview.cpp qgit.cpp rangeselectimpl.cpp \ revdesc.cpp revsview.cpp settingsimpl.cpp smartbrowse.cpp treeview.cpp \ FileHistory.cc \ common.cpp DISTFILES += app_icon.rc helpgen.sh resources/* Src.vcproj todo.txt DISTFILES += ../COPYING ../exception_manager.txt ../README ../README_WIN.txt DISTFILES += ../qgit_inno_setup.iss ../QGit4.sln # Here we generate a batch called start_qgit.bat used, under Windows only, # to start qgit with proper PATH set. # # NOTE: qgit must be installed in git directory, among git exe files # for this to work. If you install with 'make install' this is already # done for you. # # Remember to set proper GIT_EXEC_DIR value at the beginning of this file # win32 { !exists($${GIT_EXEC_DIR}/git.exe) { error("I cannot found git files, please set GIT_EXEC_DIR in 'src.pro' file") } QGIT_BAT = ../start_qgit.bat CUR_PATH = $$system(echo %PATH%) LINE_1 = $$quote(set PATH=$$CUR_PATH;$$GIT_EXEC_DIR;) LINE_2 = $$quote(set PATH=$$CUR_PATH;) qgit_launcher.commands = @echo @echo OFF > $$QGIT_BAT qgit_launcher.commands += && @echo $$LINE_1 >> $$QGIT_BAT qgit_launcher.commands += && @echo bin\\$$TARGET >> $$QGIT_BAT qgit_launcher.commands += && @echo $$LINE_2 >> $$QGIT_BAT QMAKE_EXTRA_TARGETS += qgit_launcher PRE_TARGETDEPS += qgit_launcher } qgit-2.7/src/src.qbs000066400000000000000000000073611305655150700144310ustar00rootroot00000000000000import qbs import QbsFunctions Product { type: "application" destinationDirectory: "./bin" targetName: "qgit" Depends { name: "cpp" } Depends { name: "Qt"; submodules: ["core"] } Depends { name: "Qt.gui"; condition: Qt.core.versionMajor < 5 } Depends { name: "Qt.widgets"; condition: Qt.core.versionMajor >= 5 } Probe { configure: { if (QbsFunctions.versionIsAtLeast(minimumQtVersion, Qt.core.version)) throw Error("The minimum required version Qt: " + minimumQtVersion + ". The current Qt version: " + Qt.core.version); } } cpp.cxxFlags: { var cxx = [ "-std=c++11", "-Wno-non-virtual-dtor", "-Wno-long-long", "-pedantic", ]; if (qbs.buildVariant === "debug") cxx.push("-ggdb3"); else cxx.push("-s"); if (project.conversionWarnEnabled) cxx.push("-Wconversion"); return cxx; } cpp.includePaths: [ "./", ] Group { name: "resources" files: { var files = [ "icons.qrc" ]; if (qbs.targetOS.contains('windows') && qbs.toolchain && qbs.toolchain.contains('msvc')) files.push("app_icon.rc"); if (qbs.targetOS.contains('macos') && qbs.toolchain && qbs.toolchain.contains('gcc')) files.push("resources/app_icon.rc"); return files; } } Group { name: "windows" files: [ "commitimpl.cpp", "commitimpl.h", "commit.ui", "consoleimpl.cpp", "consoleimpl.h", "console.ui", "customactionimpl.cpp", "customactionimpl.h", "customaction.ui", "fileview.cpp", "fileview.h", "fileview.ui", "help.h", "help.ui", "mainview.ui", "patchview.cpp", "patchview.h", "patchview.ui", "rangeselectimpl.cpp", "rangeselectimpl.h", "rangeselect.ui", "revsview.cpp", "revsview.h", "revsview.ui", "settingsimpl.cpp", "settingsimpl.h", "settings.ui", ] } Group { name: "others" files: [ "../exception_manager.txt", "../README", "../README_WIN.txt", "../qgit_inno_setup.iss", "helpgen.sh", "todo.txt", ] } files: [ "annotate.cpp", "annotate.h", "cache.cpp", "cache.h", "common.cpp", "common.h", "config.h", "dataloader.cpp", "dataloader.h", "domain.cpp", "domain.h", "exceptionmanager.cpp", "exceptionmanager.h", "filecontent.cpp", "filecontent.h", "filelist.cpp", "filelist.h", "git.cpp", "git.h", "inputdialog.cpp", "inputdialog.h", "lanes.cpp", "lanes.h", "listview.cpp", "listview.h", "mainimpl.cpp", "mainimpl.h", "myprocess.cpp", "myprocess.h", "patchcontent.cpp", "patchcontent.h", "revdesc.cpp", "revdesc.h", "smartbrowse.cpp", "smartbrowse.h", "treeview.cpp", "treeview.h", "FileHistory.cc", "FileHistory.h", "namespace_def.cpp", "qgit.cpp", ] //property var test: { // console.info("=== Qt.core.version ==="); // console.info(Qt.core.version); //} } qgit-2.7/src/todo.txt000066400000000000000000000003001305655150700146230ustar00rootroot00000000000000Add a QGit Icon When open history file viewer highlight all history entries in log view and stop highlight when closing.(?) RFC822-valid Subject lines which wrap across multiple text lines. qgit-2.7/src/treeview.cpp000066400000000000000000000167341305655150700154750ustar00rootroot00000000000000/* Description: files tree view Author: Marco Costalba (C) 2005-2007 Copyright: See COPYING file that comes with this distribution */ #include #include #include "git.h" #include "domain.h" #include "mainimpl.h" #include "treeview.h" QString FileItem::fullName() const { QTreeWidgetItem* p = parent(); QString s(p ? text(0) : ""); // root directory has no fullName while (p && p->parent()) { s.prepend(p->text(0) + '/'); p = p->parent(); } return s; } void FileItem::setBold(bool b) { if (font(0).bold() == b) return; QFont fnt(font(0)); fnt.setBold(b); setFont(0, fnt); } DirItem::DirItem(DirItem* p, SCRef ts, SCRef nm) : FileItem(p, nm), treeSha(ts) {} DirItem::DirItem(QTreeWidget* p, SCRef ts, SCRef nm) : FileItem(p, nm), treeSha(ts) {} void TreeView::setup(Domain* dm, Git* g) { d = dm; git = g; st = &(d->st); ignoreCurrentChanged = false; isWorkingDir = false; // set built-in pixmaps folderClosed = QGit::mimePix(".#folder_closed"); folderOpen = QGit::mimePix(".#folder_open"); fileDefault = QGit::mimePix(".#default"); connect(this, SIGNAL(itemExpanded(QTreeWidgetItem*)), this, SLOT(on_itemExpanded(QTreeWidgetItem*))); connect(this, SIGNAL(itemCollapsed(QTreeWidgetItem*)), this, SLOT(on_itemCollapsed(QTreeWidgetItem*))); connect(this, SIGNAL(currentItemChanged(QTreeWidgetItem*, QTreeWidgetItem*)), this, SLOT(on_currentItemChanged(QTreeWidgetItem*, QTreeWidgetItem*))); connect(this, SIGNAL(customContextMenuRequested(const QPoint&)), this, SLOT(on_customContextMenuRequested(const QPoint&))); } void TreeView::on_currentItemChanged(QTreeWidgetItem* item, QTreeWidgetItem*) { if (item) { SCRef fn = ((FileItem*)item)->fullName(); if (!ignoreCurrentChanged && fn != st->fileName()) { st->setFileName(fn); st->setSelectItem(true); UPDATE_DOMAIN(d); } } } void TreeView::on_customContextMenuRequested(const QPoint& pos) { QTreeWidgetItem* item = itemAt(pos); if (item) emit contextMenu(fullName(item), QGit::POPUP_TREE_EV); } void TreeView::clear() { rootName = ""; QTreeWidget::clear(); } bool TreeView::isModified(SCRef path, bool isDir) { if (isDir) return modifiedDirs.contains(path); return modifiedFiles.contains(path); } bool TreeView::isDir(SCRef fileName) { // if currentItem is NULL or is different from fileName // return false, because treeview is not updated while // not visible, so could be out of sync. FileItem* item = static_cast(currentItem()); if (item == NULL || item->fullName() != fileName) return false; return dynamic_cast(item); } const QString TreeView::fullName(QTreeWidgetItem* item) { FileItem* f = static_cast(item); return (item ? f->fullName() : ""); } void TreeView::getTreeSelectedItems(QStringList& selectedItems) { selectedItems.clear(); QList ls = QTreeWidget::selectedItems(); FOREACH (QList, it, ls) { FileItem* f = static_cast(*it); selectedItems.append(f->fullName()); } } void TreeView::setTree(SCRef treeSha) { if (topLevelItemCount() == 0) // get working directory info only once after each TreeView::clear() git->getWorkDirFiles(modifiedFiles, modifiedDirs, RevFile::ANY); QTreeWidget::clear(); treeIsValid = true; if (!treeSha.isEmpty()) { // insert a new dir at the beginning of the list DirItem* root = new DirItem(this, treeSha, rootName); expandItem(root); // be interesting } } bool TreeView::getTree(SCRef treeSha, Git::TreeInfo& ti, bool wd, SCRef treePath) { // calls qApp->processEvents() treeIsValid = git->getTree(treeSha, ti, wd, treePath); return treeIsValid; } void TreeView::on_itemCollapsed(QTreeWidgetItem* item) { item->setData(0, Qt::DecorationRole, *folderClosed); } void TreeView::on_itemExpanded(QTreeWidgetItem* itm) { DirItem* item = dynamic_cast(itm); if (!item) return; item->setData(0, Qt::DecorationRole, *folderOpen); bool alreadyWaiting = false; if (QApplication::overrideCursor()) alreadyWaiting = (QApplication::overrideCursor()->shape() == Qt::WaitCursor); if (!alreadyWaiting) QApplication::setOverrideCursor(QCursor(Qt::WaitCursor)); if (item->childCount() < 2) { QTreeWidgetItem* dummy = item->child(0); if (dummy && dummy->text(0).isEmpty()) delete dummy; // remove dummy child Git::TreeInfo ti; if (!getTree(item->treeSha, ti, isWorkingDir, item->fullName())) return; if (!ti.empty()) { Git::TreeInfo::const_iterator it(ti.constBegin()); while (it != ti.constEnd()) { const Git::TreeEntry& te = *it; if (te.type == "tree") { DirItem* dir = new DirItem(item, te.sha, te.name); dir->setData(0, Qt::DecorationRole, *folderClosed); dir->setBold(isModified(dir->fullName(), true)); new DirItem(dir, "", ""); // dummy child to show expand sign } else { FileItem* file = new FileItem(item, te.name); file->setData(0, Qt::DecorationRole, *QGit::mimePix(te.name)); file->setBold(isModified(file->fullName())); } ++it; } } } if (!alreadyWaiting) QApplication::restoreOverrideCursor(); } void TreeView::updateTree() { if (st->sha().isEmpty()) return; // qt emits currentChanged() signal when populating // the list view, so we should ignore while here ignoreCurrentChanged = true; QApplication::setOverrideCursor(Qt::WaitCursor); isWorkingDir = (st->sha() == QGit::ZERO_SHA); bool newTree = true; DirItem* root = static_cast(topLevelItem(0)); if (root && treeIsValid) newTree = (root->treeSha != st->sha()); if ( newTree && treeIsValid && root && st->sha() != QGit::ZERO_SHA && root->treeSha != QGit::ZERO_SHA) { // root->treeSha could reference a different sha from current // one in case the tree is the same, i.e. has the same files. // so we prefer to use the previous state sha to call isSameFiles() // and benefit from the early skip logic. // In case previous sha is the same of current it means an update // call has been forced, in that case we use the 'real' root->treeSha if (st->sha(true) != st->sha(false)) newTree = !git->isSameFiles(st->sha(false), st->sha(true)); else newTree = !git->isSameFiles(root->treeSha, st->sha(true)); } if (newTree) // ok, we really need to update the tree setTree(st->sha()); else { FileItem* f = static_cast(currentItem()); if (f && f->fullName() == st->fileName()) { restoreStuff(); return; } } if (st->fileName().isEmpty()) { restoreStuff(); return; } setUpdatesEnabled(false); const QStringList lst(st->fileName().split("/")); QTreeWidgetItemIterator item(this); ++item; // first item is repository name FOREACH_SL (it, lst) { while (*item && treeIsValid) { if ((*item)->text(0) == *it) { // could be a different subdirectory with the // same name that appears before in tree view // to be sure we need to check the names SCRef fn = ((FileItem*)*item)->fullName(); if (st->fileName().startsWith(fn)) { if (dynamic_cast(*item)) { expandItem(*item); ++item; } break; // from while loop only } } ++item; } } // check if st->fileName() has been deleted by a patch older than this tree if (*item && treeIsValid) { clearSelection(); setCurrentItem(*item); // calls on_currentChanged() scrollToItem(*item); } setUpdatesEnabled(true); QTreeWidget::update(); restoreStuff(); } void TreeView::restoreStuff() { ignoreCurrentChanged = false; QApplication::restoreOverrideCursor(); } qgit-2.7/src/treeview.h000066400000000000000000000040041305655150700151250ustar00rootroot00000000000000/* Description: files tree view Author: Marco Costalba (C) 2005-2007 Copyright: See COPYING file that comes with this distribution */ #ifndef TREEVIEW_H #define TREEVIEW_H #include #include "common.h" #include "git.h" class DirItem; class TreeView; class Git; class StateInfo; class Domain; class FileItem : public QTreeWidgetItem { public: FileItem(FileItem* p, SCRef nm) : QTreeWidgetItem(p, QStringList(nm)) {} FileItem(QTreeWidget* p, SCRef nm) : QTreeWidgetItem(p, QStringList(nm)) {} virtual QString fullName() const; void setBold(bool b); }; class DirItem : public FileItem { public: DirItem(QTreeWidget* parent, SCRef ts, SCRef nm); DirItem(DirItem* parent, SCRef ts, SCRef nm); protected: friend class TreeView; QString treeSha; }; class TreeView : public QTreeWidget { Q_OBJECT public: TreeView(QWidget* par) : QTreeWidget(par), d(NULL), git(NULL), treeIsValid(false) {} void setup(Domain* d, Git* g); void setTreeName(SCRef treeName) { rootName = treeName; } void updateTree(); const QString fullName(QTreeWidgetItem* item); bool isDir(SCRef fileName); bool isModified(SCRef path, bool isDir = false); void clear(); void getTreeSelectedItems(QStringList& selectedItems); bool getTree(SCRef tSha, Git::TreeInfo& ti, bool wd, SCRef tPath); const QPixmap* folderClosed; const QPixmap* folderOpen; const QPixmap* fileDefault; signals: void updateViews(const QString& newRevSha, const QString& newFileName); void contextMenu(const QString&, int type); protected slots: void on_customContextMenuRequested(const QPoint&); void on_currentItemChanged(QTreeWidgetItem*, QTreeWidgetItem*); void on_itemExpanded(QTreeWidgetItem*); void on_itemCollapsed(QTreeWidgetItem*); private: void setTree(SCRef treeSha); void setFile(SCRef fileName); void restoreStuff(); Domain* d; Git* git; StateInfo* st; QString rootName; QStringList modifiedFiles; // no need a map, should not be a lot QStringList modifiedDirs; bool ignoreCurrentChanged; bool treeIsValid; bool isWorkingDir; }; #endif