Compare commits

...
860 Commits
Author SHA1 Message Date
David Aguilar df339937f7 git-cola 1.7.0
Signed-off-by: David Aguilar <davvid@gmail.com>
2011-11-02 00:10:53 -07:00
David Aguilar 6bada56b51 Makefile: Include documentation in git-cola.app
Signed-off-by: David Aguilar <davvid@gmail.com>
2011-11-02 00:09:37 -07:00
David Aguilar 883a31e6ed main: Use the configured monospace font in the log window
Signed-off-by: David Aguilar <davvid@gmail.com>
2011-11-01 23:04:09 -07:00
David Aguilar e1be6057b9 dag: Use the brushed-metal style on OS X
Signed-off-by: David Aguilar <davvid@gmail.com>
2011-11-01 22:52:32 -07:00
David Aguilar 8bc836053b dag: Allow passing command-line arguments to git-dag
Signed-off-by: David Aguilar <davvid@gmail.com>
2011-10-31 15:55:15 -07:00
David Aguilar 365e9eb651 qt: Convert QString to unicode before comparing completion
Signed-off-by: David Aguilar <davvid@gmail.com>
2011-10-31 15:44:17 -07:00
David Aguilar 839735de76 dag: Update completions when the available refs changes
Signed-off-by: David Aguilar <davvid@gmail.com>
2011-10-28 04:52:50 -07:00
David Aguilar d4ddc6f907 dag: Close the completion popup when the dialog closes
Signed-off-by: David Aguilar <davvid@gmail.com>
2011-10-28 04:51:57 -07:00
David Aguilar 65456364c7 views: Improve window titles for git-cola and git-dag
Remove the [brackets] to make the branch name easier to read.
Make the git-dag window title useful at a glance.

Signed-off-by: David Aguilar <davvid@gmail.com>
2011-10-28 04:18:51 -07:00
David Aguilar 8d4974787c dag: Make the widget state persistent
Remember the previously entered 'count' and 'ref' values.

Signed-off-by: David Aguilar <davvid@gmail.com>
2011-10-28 04:01:05 -07:00
David Aguilar 839067e342 views: Use the common state-manipulation functions from qtutils
Signed-off-by: David Aguilar <davvid@gmail.com>
2011-10-28 04:00:04 -07:00
David Aguilar e5552b5111 qtutils: Add functions for saving and apply saved GUI state
Signed-off-by: David Aguilar <davvid@gmail.com>
2011-10-28 03:57:31 -07:00
David Aguilar 20c1fc528b qt: Show completion popup when the line edit is clicked
Signed-off-by: David Aguilar <davvid@gmail.com>
2011-10-28 03:54:12 -07:00
David Aguilar ceb17d1d20 doc: Add ijuma and Blaisorblade to the "Thanks to" file
ijuma suggested issue #33 which added the menu for reusing
previous commit messages in the message editor.

	https://github.com/git-cola/git-cola/issues/33

Blaisorblade pointed out some obvious usability bugs which
inform some of the bigger refactoring and usability
improvements in 1.7.0.

	https://github.com/git-cola/git-cola/issues/89

	https://github.com/git-cola/git-cola/issues/90

Signed-off-by: David Aguilar <davvid@gmail.com>
2011-10-27 02:05:49 -07:00
David Aguilar 1303633d45 dag: Add a standalone 'git-dag' command
The DAG visualizer can now be invoked by running:

	$ git dag

Signed-off-by: David Aguilar <davvid@gmail.com>
2011-10-27 01:59:03 -07:00
David Aguilar 23b5c0b0b7 win32: Make the Windows shortcuts say 'git-cola'
Signed-off-by: David Aguilar <davvid@gmail.com>
2011-10-27 01:24:25 -07:00
David Aguilar baedf8bf14 dag: Add documentation
Add a minimal manpage and html documentation.

Signed-off-by: David Aguilar <davvid@gmail.com>
2011-10-27 01:20:23 -07:00
David Aguilar ea0c0ef83b main: Install the command wrappers in app.main()
The command wrappers will be shared with git-dag so initialize
them in a common routine.

Signed-off-by: David Aguilar <davvid@gmail.com>
2011-10-27 00:53:48 -07:00
David Aguilar 9cfce67e5d app: Ensure sure that the debugging message is delivered
Use a QTimer to force the notification about GIT_COLA_TRACE to
the main GUI thread.

Signed-off-by: David Aguilar <davvid@gmail.com>
2011-10-27 00:50:16 -07:00
David Aguilar 3dff17e2a7 git-cola: Move the environment setup into app.main()
app.main() will be shared with git-dag so consolidate it.

Signed-off-by: David Aguilar <davvid@gmail.com>
2011-10-27 00:50:16 -07:00
David Aguilar 8c888a567c installers: Update project metadata to use the github URL
Signed-off-by: David Aguilar <davvid@gmail.com>
2011-10-27 00:50:16 -07:00
David Aguilar df3a725d94 cola: Require Qt >= 4.4
All of the conditional checks for QThreadPool can be removed.

Signed-off-by: David Aguilar <davvid@gmail.com>
2011-10-27 00:50:15 -07:00
David Aguilar e593b390f2 usability: Remove the --style option
Things look nice when git-cola follows the system theme.
Remove the unnecessary and seldom-used --style option.

Simplify the icon lookup code so that it uses Qt's resource
directories.  This lets us name icons using, e.g., 'icons:foo.png'
instead of supplying an absolute path.

Signed-off-by: David Aguilar <davvid@gmail.com>
2011-10-27 00:48:10 -07:00
David Aguilar 20707501a1 doc: Add 1.7.0 release notes draft
Signed-off-by: David Aguilar <davvid@gmail.com>
2011-10-26 20:22:21 -07:00
David Aguilar 1588c43186 gitcmds: Simplify, speedup, and refactor worktree_state()
We originally ran "git diff --exit-code <path>" against every
modified file to detect "partially-staged" files.

Simplify the code so that we only ever call two git commands to
gather staged and modified files.  This eliminates one git call
on startup in the best case and many calls in the worst case
when many files have been modified.

We may later want to split out the diff_upstream(head) call so
that it is invoked by the classic view only since it is not used
anywhere else.

Signed-off-by: David Aguilar <davvid@gmail.com>
2011-10-26 20:20:16 -07:00
David Aguilar 4ddebdbe9d version: Remove unused module
Signed-off-by: David Aguilar <davvid@gmail.com>
2011-10-26 20:15:26 -07:00
David Aguilar a42c40db09 main.view: Show "exit diff mode" when applying changes from a branch
Also set the button's minimum height so that it is not
drawn flat on OS X.

Signed-off-by: David Aguilar <davvid@gmail.com>
2011-10-26 20:14:14 -07:00
David Aguilar 43b255e0b7 usability: Move 'Rescan' to the middle of the 'actions' widget
The most frequently used items such as 'Stage' and 'Unstage' should be
listed before less-frequently used items such as 'Rescan'.

Closes #95

Signed-off-by: David Aguilar <davvid@gmail.com>
2011-10-24 15:11:48 -07:00
David Aguilar fcb3231ba0 cmds: Check if .gitignore exists before writing to it
We previously checked whether .gitignore was checked into the repository
before writing to it.  It is faster and more robust to simply check if
the file exists.  This prevents git-cola from overwriting .gitignore
when it has not been added to git.

Closes #94

Signed-off-by: David Aguilar <davvid@gmail.com>
2011-10-24 15:08:52 -07:00
David Aguilar 28dbff13f8 guicmds: Replace repobrowser with the simpler browse dialog
One less QObserver...

Signed-off-by: David Aguilar <davvid@gmail.com>
2011-10-22 21:02:06 -07:00
David Aguilar 7499d1c1f6 widgets.browse: Add a static method for selecting a single file
Signed-off-by: David Aguilar <davvid@gmail.com>
2011-10-22 20:56:23 -07:00
David Aguilar d1e5445d82 dag: Move the blob saving behavior to the browse widget
Signed-off-by: David Aguilar <davvid@gmail.com>
2011-10-22 20:38:13 -07:00
David Aguilar 9518515adb dag: Register command wrappers when standalone
git-dag will use this soon.

Signed-off-by: David Aguilar <davvid@gmail.com>
2011-10-22 20:08:24 -07:00
David Aguilar d4960d7e29 qt: Update method used for creating autocomplete matches
Signed-off-by: David Aguilar <davvid@gmail.com>
2011-10-22 15:32:37 -07:00
David Aguilar e3255b6651 dag: Make the auto-completion much more helpful
Highlight the completed text to visually indicate matches.

Change the match style so that it searches the entire entry
for a possible match instead of matching against the beginning
of the string only.

Add icons to show whether the match is a ref, file, or directory.

Signed-off-by: David Aguilar <davvid@gmail.com>
2011-10-22 14:42:11 -07:00
David Aguilar 244d5da838 utils: Add a shell_split() function and consolidate shlex.split() calls
Signed-off-by: David Aguilar <davvid@gmail.com>
2011-10-22 14:33:32 -07:00
David Aguilar c5a480733f git: Retry git commands when we get ENOMEM
We may be temporarily out of memory and thus unable to start
new sub-processeses.  Detect this condition and retry.

Signed-off-by: David Aguilar <davvid@gmail.com>
2011-10-22 14:31:39 -07:00
David Aguilar 5c92c7af4f qt: Include directory names when auto-completing paths
Signed-off-by: David Aguilar <davvid@gmail.com>
2011-10-20 16:48:12 -07:00
David Aguilar 8dc4556f95 dag: Improve autocomplete to include files
Improve the interaction with the autocompletion.
Add filenames to the list of possible completions.

Signed-off-by: David Aguilar <davvid@gmail.com>
2011-10-20 16:04:56 -07:00
David Aguilar d1c86ecf10 git-cola.app: Include documentation
Signed-off-by: David Aguilar <davvid@gmail.com>
2011-10-20 10:38:29 -07:00
David Aguilar e9e5a65fb0 dag: Zoom out a big more by default and adjust margins
Signed-off-by: David Aguilar <davvid@gmail.com>
2011-10-20 10:34:37 -07:00
David Aguilar 829b7f8c24 archive: Make the dialog wider by default
Allow enough space for a full SHA-1 plus extension.

Signed-off-by: David Aguilar <davvid@gmail.com>
2011-10-18 14:11:33 -07:00
David Aguilar 8e851e582b dag: "Save Tarball" and "Grab File..." operate on a single selected item
Disable these actions when multiple items are selected.

Signed-off-by: David Aguilar <davvid@gmail.com>
2011-10-18 10:30:09 -07:00
David Aguilar 06fa17dc72 browse: Add a key press event to provide better navigation
Add Vim-style key mappings as well as left/right-arrow
navigation shortcuts.

Signed-off-by: David Aguilar <davvid@gmail.com>
2011-10-18 05:10:24 -07:00
David Aguilar d99130aa84 dag: Add 'Grab File...' for getting an older version of a file
Signed-off-by: David Aguilar <davvid@gmail.com>
2011-10-18 05:02:51 -07:00
David Aguilar 215ae0ef5b archive: Update default filename on startup
Signed-off-by: David Aguilar <davvid@gmail.com>
2011-10-17 23:05:06 -07:00
David Aguilar 02e7b86582 archive: Make the save button enabled
It was disabled earlier before it knew how to pre-populate itself.

Signed-off-by: David Aguilar <davvid@gmail.com>
2011-10-17 22:58:45 -07:00
David Aguilar 2499192f7c archive: Seed the combobox index from git
Signed-off-by: David Aguilar <davvid@gmail.com>
2011-10-17 22:50:38 -07:00
David Aguilar ffe1691820 archive: Hand-off focus when toggling the prefix group box
Signed-off-by: David Aguilar <davvid@gmail.com>
2011-10-17 19:59:36 -07:00
David Aguilar bc1db33b38 qt: Emit a signal when toggling a QCollapsibleGroupBox
Signed-off-by: David Aguilar <davvid@gmail.com>
2011-10-17 19:58:06 -07:00
David Aguilar 2e574f82ff archive: Make the 'prefix' field operational
Signed-off-by: David Aguilar <davvid@gmail.com>
2011-10-17 13:19:17 -07:00
David Aguilar 946f9891bc archive: Select 'tar' when 'tar.gz' is not available
Signed-off-by: David Aguilar <davvid@gmail.com>
2011-10-17 11:45:21 -07:00
David Aguilar 8f8d39e075 widgets: Add an 'Advanced' section to the GitArchiveDialog
Signed-off-by: David Aguilar <davvid@gmail.com>
2011-10-17 09:39:07 -07:00
David Aguilar a9cc95c1bc dag: Add a 'Save Tarball/Zip' action
Allow exporting of tarballs from the DAG.

Signed-off-by: David Aguilar <davvid@gmail.com>
2011-10-17 04:31:18 -07:00
David Aguilar 4aee59c60c widgets: Add a widget for saving archives
Signed-off-by: David Aguilar <davvid@gmail.com>
2011-10-17 04:24:10 -07:00
David Aguilar f6c3cd30e2 status: Refactor the tree widget into a class
This makes the context menu overloading simpler since
we simply re-implement events instead of monkey-patching
them from the context of its containing widget.
The end result is simpler and easier to follow.

Signed-off-by: David Aguilar <davvid@gmail.com>
2011-10-15 13:47:28 -07:00
David Aguilar 5fded7d5f8 status: Prompt before deleting files
Prompting should be a preference but for now let's guard
against eager cat-paws.  This is a dangerous feature, after all.

Signed-off-by: David Aguilar <davvid@gmail.com>
2011-10-15 06:51:35 -07:00
David Aguilar cfcb60ea85 merge: Make global commands local
add_global_command() is only used in one place now.

Signed-off-by: David Aguilar <davvid@gmail.com>
2011-10-15 05:59:09 -07:00
David Aguilar ae19f8ca7f stash: Make rescan a local command
Eventually there should be no add_global_command() calls.

Signed-off-by: David Aguilar <davvid@gmail.com>
2011-10-15 05:28:57 -07:00
David Aguilar 66539850bd stash: Make the view use commands
Refactor the view so that it emits signals only.

Signed-off-by: David Aguilar <davvid@gmail.com>
2011-10-15 04:31:18 -07:00
David Aguilar 64a554ecef stash: Make the 'index' checkbox work for 'apply'
There are times with 'git apply --index' fails but
'git apply' does not so allow the checkbox used for 'save'
to also affect the 'apply' operation.

Signed-off-by: David Aguilar <davvid@gmail.com>
2011-10-15 04:31:18 -07:00
David Aguilar f35fa7e288 ctrl: Add method to add a set of commands at once
Signed-off-by: David Aguilar <davvid@gmail.com>
2011-10-15 04:31:18 -07:00
David Aguilar cde7b1e6ef git: Add "git" convenience variable
Signed-off-by: David Aguilar <davvid@gmail.com>
2011-10-15 04:31:17 -07:00
David Aguilar 6aaad58f64 sphinxtogithub: Do not error out on older versions of sphinx
Disable sphinxtogithub if sphinx is too old.
Issue a warning when it happens.

Signed-off-by: David Aguilar <davvid@gmail.com>
2011-10-15 04:31:17 -07:00
David Aguilar 7849a83bb1 main.view: Register 'Ctrl+R' for refresh only
Avoid ambiguous shortcut overloads by using 'Ctrl+R' directly
and avoiding the platform-dependent QKeySequence.Refresh.
One some platforms QKeySequence.Refresh != 'Ctrl+R' and the
shortcut is not overloaded there.  On Linux they are equal
which is what creates the overloaded shortcut.

Signed-off-by: David Aguilar <davvid@gmail.com>
2011-10-13 16:42:53 -07:00
David Aguilar 0a429d9098 README: Complete the strange self-referential loop
Signed-off-by: David Aguilar <davvid@gmail.com>
2011-10-10 23:47:20 -07:00
David Aguilar 49422f5f17 doc: Update homepage URL and thanks section
Signed-off-by: David Aguilar <davvid@gmail.com>
2011-10-10 23:31:20 -07:00
David Aguilar 494a4cedd6 doc: Use http:// instead of https:// for the homepage
Github does not offer https:// hosting so fix the URLs.

Signed-off-by: David Aguilar <davvid@gmail.com>
2011-10-10 12:32:48 -07:00
David Aguilar a5528a8e1d doc: Use the haiku sphinx theme
Signed-off-by: David Aguilar <davvid@gmail.com>
2011-10-09 05:40:49 -07:00
David Aguilar 50b5abdccf doc: Do not delete conf.py
This file is no longer generated by the Makefile.

Signed-off-by: David Aguilar <davvid@gmail.com>
2011-10-05 16:59:04 -07:00
David Aguilar 5e416ed523 dag: Avoid mapRectToScene for compatibility with older Qt versions
Do the mapping ourself to allow cola to use older versions of Qt.

Signed-off-by: David Aguilar <davvid@gmail.com>
2011-10-01 14:03:31 -07:00
David Aguilar 607c755d79 Makefile: Remove extra work done in "make dist"
Our tarballs do not need to include the extra "version"
file anymore so remove the unnecessary steps from the dist target.

This target won't really be used anymore since we are just
publishing the repo as-is without any post-processing of the
output of "git archive", but it's still nice to have.

Signed-off-by: David Aguilar <davvid@gmail.com>
2011-10-01 13:20:47 -07:00
David Aguilar 662ef03f9f version: Do not look for a 'version' file anymore
We now keep a default version in the 'version.py' file
so avoid relying on the extra step of creating a 'version'
file at the root of the project.

Signed-off-by: David Aguilar <davvid@gmail.com>
2011-10-01 13:18:05 -07:00
David Aguilar 79dc6a7c79 dag: Allow passing in arbitrary "git log" options
The text input field can now be used like a command-line
whose arguments are passed along to "git log".

This allows us to slice and dice history using the standard
gitcli(1) shell syntax.

The GUI now displays "git log -# [...]" which is familiar and
powerful for git users.

Signed-off-by: David Aguilar <davvid@gmail.com>
2011-10-01 01:27:57 -07:00
David Aguilar 64d5cc74b9 dag: Better support for viewing slices of history
The DAG view accepts arbitrary options such as
"master -- Makefile" which are passed along to "git log".
This means that much of the history can be missing.

Adjust the commit generation numbers according to a simple counter
for commits adjacent to missing history.

Signed-off-by: David Aguilar <davvid@gmail.com>
2011-10-01 01:23:07 -07:00
David Aguilar 824f604814 dag: Guard against division by zero when adjusting scrollbars
Signed-off-by: David Aguilar <davvid@gmail.com>
2011-10-01 01:23:07 -07:00
David Aguilar 10135bc0c7 main: Remove redundant browse dialog
Less is more.  The DAG viewer is better.

Signed-off-by: David Aguilar <davvid@gmail.com>
2011-10-01 00:12:22 -07:00
David Aguilar b0f36b7458 doc: Update 'davvid' links to point to 'git-cola'
Update all the github URLs to point to the new 'git-cola' organization.

Signed-off-by: David Aguilar <davvid@gmail.com>
2011-09-30 09:53:23 -07:00
David Aguilar 2b3dc1f862 INSTALL: Mention brew instead of deprecated packages
Signed-off-by: David Aguilar <davvid@gmail.com>
2011-09-30 00:52:25 -07:00
David Aguilar 653cb57084 README: Update github links and description
Signed-off-by: David Aguilar <davvid@gmail.com>
2011-09-30 00:31:57 -07:00
David Aguilar 40cb20bef0 doc: Add sphinxtogithub extension
Signed-off-by: David Aguilar <davvid@gmail.com>
2011-09-30 00:03:00 -07:00
David Aguilar 9d172acde3 version: Provide a default version
Signed-off-by: David Aguilar <davvid@gmail.com>
2011-09-29 23:36:52 -07:00
David Aguilar 41b4d618f5 qtcompat: Add compatibility for QtGui.QKeySequence.Preferences
Qt 4.6 does not provide QtGui.QKeySequence.Preferences; add it.
Guard against duplicate shortcut definitions by uniqifying the list.

Signed-off-by: David Aguilar <davvid@gmail.com>
2011-09-28 10:07:28 -07:00
David Aguilar 5a73d32298 dag: Speed up drawing by setting some QGraphicsView rendering flags
Signed-off-by: David Aguilar <davvid@gmail.com>
2011-09-28 09:49:19 -07:00
David Aguilar 040b5bff6a widgets.commitmsg: Add a menu for loading a previous commit message
Add an item to the context menu for loading previous commit mesages.
Five items are displayed.  Additional message can be selected by
choosing the "More..." menu item.

Closes #33

Signed-off-by: David Aguilar <davvid@gmail.com>
2011-09-28 09:46:26 -07:00
David Aguilar d0db763fb3 dag.model: Implement __len__()
Signed-off-by: David Aguilar <davvid@gmail.com>
2011-09-28 09:46:26 -07:00
David Aguilar ca965f9154 widgets.diff: Remove unneeded action
Remove vestigal action from the refactoring.

Signed-off-by: David Aguilar <davvid@gmail.com>
2011-09-28 09:46:26 -07:00
David Aguilar ed276302aa cmds: Add a command for loading previous commit messages
Signed-off-by: David Aguilar <davvid@gmail.com>
2011-09-28 09:06:19 -07:00
David Aguilar 4d2f283783 main: Use the improved stash dialog
Remove the old stash dialog and stash-related methods from
the main model.  Switch to using the better stash dialog.

Signed-off-by: David Aguilar <davvid@gmail.com>
2011-09-26 22:14:16 -07:00
David Aguilar 335125d2bc stash: Add a simplified and improved stash dialog
The stash dialog now has an inline stash viewer instead of
using the diff viewer from the main UI.  It also uses the
simplified MVC architecture instead of depending on QObserver.

Signed-off-by: David Aguilar <davvid@gmail.com>
2011-09-26 19:54:53 -07:00
David Aguilar 021892b68b stash: Add a cola.stash package
The simpler stash dialog will live here.

Signed-off-by: David Aguilar <davvid@gmail.com>
2011-09-26 19:53:57 -07:00
David Aguilar c291f82054 merge: Remove qtutils usage in the model
Use the notifier instead of calling into Qt directly.

Signed-off-by: David Aguilar <davvid@gmail.com>
2011-09-26 19:53:57 -07:00
David Aguilar a5a7bc702c widgets: Let the diff and commit message editors set the diff font
Handle setting the font during construction.
Factor out the read-only diff viewer into a standalone class.

Signed-off-by: David Aguilar <davvid@gmail.com>
2011-09-26 19:53:57 -07:00
David Aguilar a2cc39ef5b widgets: Move the RevisionSelector to cola.widgets.revselect
Signed-off-by: David Aguilar <davvid@gmail.com>
2011-09-26 17:33:48 -07:00
David Aguilar 0577622570 widgets: Move the StartupDialog to cola.widgets.startup
Signed-off-by: David Aguilar <davvid@gmail.com>
2011-09-26 17:28:37 -07:00
David Aguilar b87b81ce5a widgets: Move the CommitMessageEditor to cola.widgets.commitmsg
Signed-off-by: David Aguilar <davvid@gmail.com>
2011-09-26 17:20:38 -07:00
David Aguilar f8decacb36 widgets.cfgactions: Remove global references
Remove the global "instances" data structure since it is not
needed to prevent Python from garbage collecting the widget.
The widget is kept in scope naturally and thus does not need
us to keep an extra reference.

Signed-off-by: David Aguilar <davvid@gmail.com>
2011-09-26 17:12:20 -07:00
David Aguilar 9be68a937f qt: Add a GitRefLineEdit and use it where appropriate
Add a QLineEdit-derived widget that comes with a GitRefCompleter
built-in.  This lets us avoid repeating the same setup code in
various places.

Signed-off-by: David Aguilar <davvid@gmail.com>
2011-09-26 17:08:28 -07:00
David Aguilar 5b6a369834 qt: Remove observers when GitRefCompleters are deleted
Avoid sending model notifications to dead C++ objects by
removing observers during object deletion.

Signed-off-by: David Aguilar <davvid@gmail.com>
2011-09-26 17:05:59 -07:00
David Aguilar 5eaef7d297 widgets: Fold cola.views.command into cola.widgets.cfgactions
There's no reason for these to be separate modules.

Signed-off-by: David Aguilar <davvid@gmail.com>
2011-09-26 16:52:06 -07:00
David Aguilar e1e68a1279 cleanup: Remove all uses of the 'commands' module
Signed-off-by: David Aguilar <davvid@gmail.com>
2011-09-26 16:41:06 -07:00
David Aguilar 9fffc28d34 dag: Workaround win32 not supporting NULL in format strings
There must be a bug in msysgit because setting the file
descriptors to binary does not allow NULL in git log's
output when using a %x00 in the format string.

Signed-off-by: David Aguilar <davvid@gmail.com>
2011-09-26 16:18:51 -07:00
David Aguilar 61cf00167d widgets: Move the config action widgets to cola.widgets.cfgactions
Signed-off-by: David Aguilar <davvid@gmail.com>
2011-09-26 21:00:07 +02:00
David Aguilar d9ce092839 cmds: Properly pass filenames to config actions
There's no need to call commands.mkarg.

Signed-off-by: David Aguilar <davvid@gmail.com>
2011-09-26 21:00:07 +02:00
David Aguilar 16ab711746 widgets: Move the combo dialog to cola.widgets.combodlg
Signed-off-by: David Aguilar <davvid@gmail.com>
2011-09-26 21:00:07 +02:00
David Aguilar 756d372646 widgets: Move syntax highlighter to cola.widgets.highlighter
Signed-off-by: David Aguilar <davvid@gmail.com>
2011-09-26 17:39:32 +02:00
David Aguilar ca8fb6e77b widgets: Move the list view to cola.widgets.listview
Signed-off-by: David Aguilar <davvid@gmail.com>
2011-09-26 17:36:30 +02:00
David Aguilar 43b2655835 widgets: Move the log widget to cola.widgets.log
Signed-off-by: David Aguilar <davvid@gmail.com>
2011-09-26 17:32:21 +02:00
David Aguilar 4eb5a868cd widgets: Move the about dialog to cola.widgets.about
Signed-off-by: David Aguilar <davvid@gmail.com>
2011-09-26 17:29:55 +02:00
David Aguilar 58ca064814 widgets: Move the status widget to cola.widgets.status
Signed-off-by: David Aguilar <davvid@gmail.com>
2011-09-26 17:29:50 +02:00
David Aguilar 43d4a79170 widgets: Move the diff widget to cola.widgets.diff
Signed-off-by: David Aguilar <davvid@gmail.com>
2011-09-26 17:29:18 +02:00
David Aguilar 17ad34e455 Add a cola.widgets package
Widgets that do not need a complete MVC setup will live here.

Signed-off-by: David Aguilar <davvid@gmail.com>
2011-09-26 17:14:40 +02:00
David Aguilar 0ed0711977 qt: Move the DiffSyntaxHighlighter into the cola.qt module
Signed-off-by: David Aguilar <davvid@gmail.com>
2011-09-26 17:11:22 +02:00
David Aguilar a18961f51d dag: Remove trailing newline in the commit subject
The list widget items were one row too tall because the text
being shown ended with a newline.  Call rstrip() after
readline() to remove it.

Signed-off-by: David Aguilar <davvid@gmail.com>
2011-09-26 16:10:52 +02:00
David Aguilar 26b26b7059 dag: Make sys.{stdin,stdout,stderr} descriptors binary on win32
The DAG worked around stdout not returning binary data by using
0x01 instead of NULL as a separator in its "git log" format string.

Rework it so that the file descriptors are binary and use NULL in
the format string again.

Signed-off-by: David Aguilar <davvid@gmail.com>
2011-09-26 15:53:00 +02:00
David Aguilar 27fdb43ced status: Remove the platform check when launching mergetool
There is no need to check the platform at runtime now that we
can rely on "git mergetool --no-prompt" being available.

Signed-off-by: David Aguilar <davvid@gmail.com>
2011-09-26 15:33:18 +02:00
David Aguilar 66d5937d23 main: Use the new merge dialog and remove the old one
Signed-off-by: David Aguilar <davvid@gmail.com>
2011-09-26 15:05:00 +02:00
David Aguilar 5763ce77f0 merge: Add a new merge dialog which uses the MVC framework
This fleshes out the cola.merge package so that it contains the
customary model, view, and controller modules.  This is a drop-in
replacement for old dialog.

Signed-off-by: David Aguilar <davvid@gmail.com>
2011-09-26 15:05:00 +02:00
David Aguilar fd07da80d4 Add a cola.merge package
The merge dialog will live here.

Signed-off-by: David Aguilar <davvid@gmail.com>
2011-09-26 15:04:53 +02:00
David Aguilar 17e91cc027 cmds: Add a VisualizeRevision command
This will be used by the new merge dialog.

Signed-off-by: David Aguilar <davvid@gmail.com>
2011-09-26 15:04:29 +02:00
David Aguilar 96aa745c96 utils: Remove unused functions and fixup style
Remove the unused notify() and debug() functions.
Make the other functions follow PEP-8.

Signed-off-by: David Aguilar <davvid@gmail.com>
2011-09-26 15:04:29 +02:00
David Aguilar 2b8fc80ec2 utils: Remove all remnants of quote_repopath()
This was a workaround for win32 which is no longer needed now that
we use subprocess when backgrounding commands.

Signed-off-by: David Aguilar <davvid@gmail.com>
2011-09-26 15:04:29 +02:00
David Aguilar a4de84fda7 test: Remove pointless test case
We had a test case to prevent circular imports but it's pointless
and gets in the way when refactoring.  It is not needed since we
will simply not introduce any circular imports.

Signed-off-by: David Aguilar <davvid@gmail.com>
2011-09-26 15:04:29 +02:00
David Aguilar cae8704e09 utils: Use subprocess to launch background commands
This removes all of the confusing and problematic win32 hacks
by using the simple Popen(cmd].pid pattern.

Signed-off-by: David Aguilar <davvid@gmail.com>
2011-09-26 15:04:29 +02:00
David Aguilar d3aa1f1b7b app: Register commands in ColaApplication
Avoid needing to duplicate cmds.register() by doing it in the
ColaApplication singleton.

Signed-off-by: David Aguilar <davvid@gmail.com>
2011-09-26 15:04:29 +02:00
David Aguilar f7bbd7e3bc dag: Use the GitRefCompleter from the cola.qt package
Signed-off-by: David Aguilar <davvid@gmail.com>
2011-09-25 20:34:30 +02:00
David Aguilar 541f061ad9 qt: Add an enhanced GitRefCompelter to the cola.qt package
Use a QStringListModel internally instead of setting the list of
revisions during object construction.  Subscribe to the model's
"updated" message so that changes to the git data is reflected
in the completer.

Signed-off-by: David Aguilar <davvid@gmail.com>
2011-09-25 20:33:59 +02:00
David Aguilar b953490d57 setup.py: Add missing packages
The recent package refactoring forgot to update setup.py.
Update it.

Closes #91

Reported-by: Francisco Ruiz <franciscoruiz@2degreesnetwork.com>
Signed-off-by: David Aguilar <davvid@gmail.com>
2011-09-19 20:02:41 +02:00
David Aguilar 177c1167df prefs: Switch context using a tab bar
Minimize the number of clicks when editing preferences by using
a tab bar instead of a combo box.

Closes #89

Suggested-by: Paolo G. Giarrusso <Blaisorblade on github>
Signed-off-by: David Aguilar <davvid@gmail.com>
2011-09-16 09:55:54 +02:00
David Aguilar b0589e11ea classic: Move the classic code into the cola.classic package
Signed-off-by: David Aguilar <davvid@gmail.com>
2011-09-13 06:01:40 +02:00
David Aguilar 606f135670 doc: Remove API documentation while it is still in flux
Remove the Sphinx API documentation while we refactor cola into
its final shape.

Signed-off-by: David Aguilar <davvid@gmail.com>
2011-09-13 05:54:09 +02:00
David Aguilar 6939cb71af dag: Change button order to Zoom Out / Zoom In
Signed-off-by: David Aguilar <davvid@gmail.com>
2011-09-13 05:28:35 +02:00
David Aguilar 6b072767f2 dag: Move the DAG code into the cola.dag package
Signed-off-by: David Aguilar <davvid@gmail.com>
2011-09-12 20:24:16 -07:00
David Aguilar d211d55fbe main: Remove unused signal and alphabetize imports
Signed-off-by: David Aguilar <davvid@gmail.com>
2011-09-12 16:42:15 -07:00
David Aguilar 03ab7169d4 main: Combine the mainwindow and view modules
Signed-off-by: David Aguilar <davvid@gmail.com>
2011-09-12 16:32:08 -07:00
David Aguilar eb12e90516 cola: Refactor the main classes out into the cola.main package
The main entry point now lives at cola.app.main() instead of
cola.main.main() to package name conflicts.

This is the first step towards reorganizing the project into
separate packages for each tool/dialog.

Signed-off-by: David Aguilar <davvid@gmail.com>
2011-09-12 16:16:30 -07:00
David Aguilar 573f0e6cbd prefs: Swap the options dialog for the simpler preferences dialog
The new preferences dialog is simpler and does not depend on the
(deprecated) QObserver controller class.

Closes #89

Signed-off-by: David Aguilar <davvid@gmail.com>
2011-09-12 15:26:47 -07:00
David Aguilar 37e7ef0659 prefs: Use prefs.diff_font() instead of qtutils.set_diff_font()
Centralize all of the preferences for fonts into the prefs module
and use it instead of the qtutils function.

Signed-off-by: David Aguilar <davvid@gmail.com>
2011-09-12 15:20:46 -07:00
David Aguilar 99e6bd2c0b prefs: Add accessors for the diff font and tab width
Signed-off-by: David Aguilar <davvid@gmail.com>
2011-09-12 15:18:00 -07:00
David Aguilar 226be2e5da prefs.model: Add notification for config changes
Signed-off-by: David Aguilar <davvid@gmail.com>
2011-09-12 15:17:21 -07:00
David Aguilar 30e7cada55 prefs: Add ability to change font settings
Signed-off-by: David Aguilar <davvid@gmail.com>
2011-09-12 11:24:53 -07:00
David Aguilar c17187528e prefs: Add a standalone preferences editor
This will replace the QObserver-based Options editor.

Signed-off-by: David Aguilar <davvid@gmail.com>
2011-09-12 05:30:53 -07:00
David Aguilar c4eebda10d views.main: Factor out more widgets from the main window
Make everything notification-based so the main view does not depend
on the QObserver magic.  The diff viewer and commit message editor are
now completely independent widgets that are only loosely coupled to
the main window.

Signed-off-by: David Aguilar <davvid@gmail.com>
2011-09-12 05:30:53 -07:00
David Aguilar 60a888826c qt: Add helpers for creating dockwidgets and menus
Signed-off-by: David Aguilar <davvid@gmail.com>
2011-09-12 05:30:53 -07:00
David Aguilar c27488d3ff i18n: Use qtutils.tr() instead of i18n.gettext()
Signed-off-by: David Aguilar <davvid@gmail.com>
2011-09-12 05:30:52 -07:00
David Aguilar a9f9ce2f1d gitcfg: Make the config class observable
Signed-off-by: David Aguilar <davvid@gmail.com>
2011-09-12 05:30:52 -07:00
David Aguilar f60f03a3c8 views: Use addLayout() instead of addItem() where appropriate
Signed-off-by: David Aguilar <davvid@gmail.com>
2011-09-12 05:30:52 -07:00
David Aguilar e6b874bfe3 notification: Documentation update and misc. style fixes
Signed-off-by: David Aguilar <davvid@gmail.com>
2011-09-12 05:30:52 -07:00
David Aguilar 11f1ee5ad5 cmds: Factor out a simpler base class
Signed-off-by: David Aguilar <davvid@gmail.com>
2011-09-12 05:30:52 -07:00
David Aguilar 46b9617705 cmds: Add signoff and diff-text modifying commands
Signed-off-by: David Aguilar <davvid@gmail.com>
2011-09-12 05:30:52 -07:00
David Aguilar 5fa239359a cmdfactory: Add support for context-sensitive commands
Signed-off-by: David Aguilar <davvid@gmail.com>
2011-09-12 05:30:52 -07:00
David Aguilar 0e7de977ea cmdfactory: Fix mistyped 'cmdobj'
Signed-off-by: David Aguilar <davvid@gmail.com>
2011-09-12 05:30:52 -07:00
David Aguilar efa719451c qtutils: Add helpers for signal routing and widget creation
Signed-off-by: David Aguilar <davvid@gmail.com>
2011-09-12 05:30:52 -07:00
David Aguilar 0feda99acd cola: Add a controller base class 2011-09-12 05:30:52 -07:00
David Aguilar 3ed3b6e31b qtutils: Add Ctrl+Q as a shortcut for 'Close'
Signed-off-by: David Aguilar <davvid@gmail.com>
2011-09-10 04:04:04 -07:00
David Aguilar c4cdcdb7b5 difftool: Add a small margin around the file list
Add a margin around the file list used when diffing two
commits.

Signed-off-by: David Aguilar <davvid@gmail.com>
2011-09-10 03:55:56 -07:00
David Aguilar 5381dec8c0 views.main: Make the diff viewer into a standalone widget
Make the main window know nothing about the diff viewer by
moving all of the diff editing functionality into a standalone
DiffTextEdit widget.

Signed-off-by: David Aguilar <davvid@gmail.com>
2011-09-10 03:51:29 -07:00
David Aguilar a098be0814 views.main*: Simplify menus, reduce code, and fix Ctrl+C in diff viewer
Remove the unnecessary 'Edit' menu from the main menu bar.
The 'Options' action moved into the 'File' menu since the
'Edit' menu is empty without Cut/Paste/etc.

Centralize all of the action handling code into views.mainwindow.
Do not register any actions for Copy/Paste/etc. whatsoever so that
the Qt defaults (which do the right thing) are used instead.

This is the simpler thing to do, uses less code, makes a nicer UI,
and fixes being able to use Ctrl+C in the diff viewer.

It also reduces confusion because it is not clear for new users whether
the edit menu operates on the commit message editor or on the diff viewer.

Likewise, leaving the Ctrl+C, etc. keyboard shortcuts alone allows
Qt to use the correct widget-context action (which happens by default).

This is a net reduction of ~100 lines of code. yay :-)

Closes #90

Signed-off-by: David Aguilar <davvid@gmail.com>
2011-09-09 23:45:19 -07:00
David Aguilar aadc1acd79 dag: Make it easier to see selected commits
Use a darker blue brush to make selected commits stand out.

Signed-off-by: David Aguilar <davvid@gmail.com>
2011-09-09 09:26:36 -07:00
David Aguilar dda8093171 dag: Add 'Cherry Pick' action
Signed-off-by: David Aguilar <davvid@gmail.com>
2011-09-08 18:37:35 -07:00
David Aguilar fa27e5bdf8 dag: Rename 'notifier' variable for consistency
Signed-off-by: David Aguilar <davvid@gmail.com>
2011-09-08 18:37:23 -07:00
David Aguilar 479732ce3f dag: Add a 'Create Tag' action
Signed-off-by: David Aguilar <davvid@gmail.com>
2011-09-08 18:24:37 -07:00
David Aguilar 53f3847529 dag: Consolidate context_menu_event() across both widgets
Signed-off-by: David Aguilar <davvid@gmail.com>
2011-09-08 18:24:22 -07:00
David Aguilar 8e22859df9 dag: Consolidate update_actions() across both widgets
Signed-off-by: David Aguilar <davvid@gmail.com>
2011-09-08 18:23:50 -07:00
David Aguilar 95bbba6315 dag: Add 'Create Branch' action to the tree and graph widgets
Signed-off-by: David Aguilar <davvid@gmail.com>
2011-09-08 17:39:07 -07:00
David Aguilar 0658b1477e dag: Rename commit/item variables for consistency
Refer to scene items as "items" and commit nodes as Commits consistenly.
This makes the terminology consistent between the tree and graph widgets.

Signed-off-by: David Aguilar <davvid@gmail.com>
2011-09-08 16:33:28 -07:00
David Aguilar b4690e4cf8 dag: s/nodecom/notifier/g
Signed-off-by: David Aguilar <davvid@gmail.com>
2011-09-08 16:21:32 -07:00
David Aguilar 74fc70c16c dag: Guard against empty text in the revision field
Signed-off-by: David Aguilar <davvid@gmail.com>
2011-09-08 16:15:19 -07:00
David Aguilar 0569081815 dag: Add diff actions to the graph widget
Hook up the right-click actions for diffing commits to the
difftool.diff_commits() helper.

Signed-off-by: David Aguilar <davvid@gmail.com>
2011-09-08 16:08:18 -07:00
David Aguilar b137ac0ae1 dag: Add diff actions to the tree widget
Hook up the right-click actions for diffing commits to the
difftool.diff_commits() helper.

Signed-off-by: David Aguilar <davvid@gmail.com>
2011-09-08 15:57:43 -07:00
David Aguilar c5827f1efd difftool: Add a dialog for diffing commits
Signed-off-by: David Aguilar <davvid@gmail.com>
2011-09-08 15:52:37 -07:00
David Aguilar 2840343cea dag: Show a diff when starting up
Keep track of commit SHA-1s in the GitDAGWidget so that we broadcast
a selection event to all listeners.

Signed-off-by: David Aguilar <davvid@gmail.com>
2011-09-08 13:00:03 -07:00
David Aguilar b41904baf9 dag: Add 'Create Patch' to the tree widget
Signed-off-by: David Aguilar <davvid@gmail.com>
2011-09-08 03:38:08 -07:00
David Aguilar 3e9f1613d1 dag: Add stub actions for diffing commits
Signed-off-by: David Aguilar <davvid@gmail.com>
2011-09-08 03:23:40 -07:00
David Aguilar 00a72f22f7 dag: Synchronize multi-select across tree and graph widgets
Signed-off-by: David Aguilar <davvid@gmail.com>
2011-09-08 03:21:16 -07:00
David Aguilar aa2006777f dag: Add 'Create Patch' to the graph view
The context menu event can now export patches for selected commits.

Signed-off-by: David Aguilar <davvid@gmail.com>
2011-09-08 01:56:32 -07:00
David Aguilar 0a6288aadc cmds: Guard against an empty list of files in the Diff command
It is possible for a diff command to be issued with an empty
list of files.  Guard against this condition so that we do not
generate a stacktrace and hang.

Closes #88

Signed-off-by: David Aguilar <davvid@gmail.com>
2011-09-07 11:20:12 -07:00
David Aguilar 4ce1fca204 qtutils: Use QIcon.fromTheme() in theme_icon()
Avoid using GTK by using Qt's built-in QIcon.fromTheme().
Support older versions of Qt by catching AttributeError and
falling back to our built-in icons.

Signed-off-by: David Aguilar <davvid@gmail.com>
2011-09-07 10:29:49 -07:00
David Aguilar 62bd2f5255 dag: Use the zoom in/out icons from the user's theme
Signed-off-by: David Aguilar <davvid@gmail.com>
2011-09-06 18:46:46 -07:00
David Aguilar a57333116b dag: Use better zoom in/out icons
Signed-off-by: David Aguilar <davvid@gmail.com>
2011-09-06 18:44:18 -07:00
David Aguilar 4af7a6538a dag: Add completion for branches and tags
Make the revision line edit provides auto-completion for branches and tags.

Signed-off-by: David Aguilar <davvid@gmail.com>
2011-09-05 12:55:29 -07:00
David Aguilar 9fe67894af dag: Display the current branch at startup
Signed-off-by: David Aguilar <davvid@gmail.com>
2011-09-05 12:54:10 -07:00
David Aguilar 98a5533174 dag: Support unicode branches, tags, and subject lines
Signed-off-by: David Aguilar <davvid@gmail.com>
2011-09-05 12:44:57 -07:00
David Aguilar a30b627be4 packaging: Depend on git >= v1.6.3
git-difftool originated in git-cola and was later contributed to
the main git project's contrib/ section.  It moved out of contrib/
in git v1.6.3.

git-cola also uses "git mergetool --no-prompt" and "git diff --patience"
which both appeared in v1.6.2.

Remove our version of difftool and depend on git >= v1.6.3 so that
we do not have to maintain these scripts anymore.  Remove the runtime
checks for "mergetool --no-prompt" and "diff --patience" now that the
entire project depends on a modern git.

Signed-off-by: David Aguilar <davvid@gmail.com>
2011-09-04 19:25:47 -07:00
David Aguilar cedb6e461e dag: Add GUI controls for zoom in/out
Signed-off-by: David Aguilar <davvid@gmail.com>
2011-09-04 19:05:54 -07:00
David Aguilar 5e0ebbba3d dag: Retain scrollbar position when zooming in/out
Signed-off-by: David Aguilar <davvid@gmail.com>
2011-09-04 19:05:26 -07:00
David Aguilar c232364b2a dag: Hook up GUI controls for displaying history
The "max results" spinbox and "revision" line edit now control
the displayed history.  The "Display" button clears the graph
and starts a new query.

Signed-off-by: David Aguilar <davvid@gmail.com>
2011-09-04 19:04:47 -07:00
David Aguilar f5a08c6e6b dag: Support unicode in log messages and author names
Signed-off-by: David Aguilar <davvid@gmail.com>
2011-09-04 18:36:40 -07:00
David Aguilar d81efa06e5 dag: Synchronize selection between graph and tree widgets
Signed-off-by: David Aguilar <davvid@gmail.com>
2011-09-03 19:53:00 -07:00
David Aguilar 9c8b495d98 dag: Fit view to the selected node on startup
Signed-off-by: David Aguilar <davvid@gmail.com>
2011-09-03 02:11:51 -07:00
David Aguilar f951a3d306 dag: Make the graph take half of the screen only
Uri was right (as usual).

Signed-off-by: David Aguilar <davvid@gmail.com>
2011-09-03 02:05:49 -07:00
David Aguilar aebec48195 dag: Resize tree when the main splitter moves
I forgot to update this when we reworked the layout.

Signed-off-by: David Aguilar <davvid@gmail.com>
2011-09-03 01:52:06 -07:00
David Aguilar 1a82ee4db3 dag: Make the layout vertical
Place the controls on the left and the graph on the right.

Signed-off-by: David Aguilar <davvid@gmail.com>
2011-09-02 14:48:43 -07:00
David Aguilar f6fff51d7d dag: Adjust column sizes whenever the log treewidget is resized
Signed-off-by: David Aguilar <davvid@gmail.com>
2011-09-02 00:08:32 -07:00
David Aguilar e33bd3cc15 dag: Adjust widget sizes in the show() callback
This makes sure things look good when standalone and when called from
git-cola's Tools menu.

Signed-off-by: David Aguilar <davvid@gmail.com>
2011-09-02 00:00:24 -07:00
David Aguilar 7671ad28dd dag: Show merge details in the diff widget
Determine whether or not to include merge information when displaying
a commit in the diff widget.

Signed-off-by: David Aguilar <davvid@gmail.com>
2011-09-01 23:48:25 -07:00
David Aguilar 0e90c26e2e dag: Do not show node in the diff view when rubber-band selecting
Signed-off-by: David Aguilar <davvid@gmail.com>
2011-09-01 23:46:42 -07:00
David Aguilar 5ca21751e0 dag: Add a list view for the log output
Signed-off-by: David Aguilar <davvid@gmail.com>
2011-09-01 23:46:07 -07:00
David Aguilar 0388df418c dag: Restore ability to select nodes using the left mouse button
Signed-off-by: David Aguilar <davvid@gmail.com>
2011-08-26 19:22:41 -07:00
David Aguilar ecaf80a17a dag: Add a context menu
This will eventually do operations on the selection.

Signed-off-by: David Aguilar <davvid@gmail.com>
2011-08-26 09:27:24 -07:00
David Aguilar 06f8519e19 dag: Micro-optimize paint methods
Signed-off-by: David Aguilar <davvid@gmail.com>
2011-08-26 09:26:38 -07:00
David Aguilar 18c6d7ff69 dag: Rework background thread to process more commits at once
We were sending commits from the reader thread once per commit.
This floods the main GUI with signals.  Throttle it back by
processing commits in larger chunks.

Signed-off-by: David Aguilar <davvid@gmail.com>
2011-08-26 09:23:58 -07:00
David Aguilar 01de62f578 dag: Make labels into a separate item class
The label drawing code was originally part of the Node item class.
This had the negative side-effect that clicking on the label would
select the node even when the node contained no labels.

Use a separate, non-selectable, QGraphicsItem subclass for labels.
Make it a child of the node so that it can be positioned relative
to the node's position in the scene graph.

Signed-off-by: David Aguilar <davvid@gmail.com>
2011-08-25 20:55:05 -07:00
David Aguilar 28c696380a dag: Use a pointing-hand cursor for commits
Signed-off-by: David Aguilar <davvid@gmail.com>
2011-08-25 20:55:02 -07:00
David Aguilar a187d3a8dd dag: Make nodes taller
Signed-off-by: David Aguilar <davvid@gmail.com>
2011-08-25 03:37:22 -07:00
David Aguilar 45ce3b4a09 version: Only run git describe when .git exists
Signed-off-by: David Aguilar <davvid@gmail.com>
2011-08-25 03:22:17 -07:00
David Aguilar 7e721943c0 views.dag: Add navigation shortcuts for "J", "Shift+J", "K", and "Shift+K"
J and K navigate through a node's relatives.  These shortcuts match vim.

Signed-off-by: David Aguilar <davvid@gmail.com>
2011-08-24 04:38:48 -07:00
David Aguilar 71b8d795d2 views.dag: Use QActions instead of overriding keyPressEvent()
Attach shortcuts using QActions instead of playing keyPressEvent games.

Signed-off-by: David Aguilar <davvid@gmail.com>
2011-08-24 03:35:10 -07:00
David Aguilar 2d3ce6be44 views.dag: Adjust padding around scene rect
Signed-off-by: David Aguilar <davvid@gmail.com>
2011-08-24 03:32:50 -07:00
David Aguilar 7efb6ea23d views.dag: Select HEAD on startup
Move the "commit selected" callback into itemChange().
Set the 'HEAD' node selected when starting up.

Signed-off-by: David Aguilar <davvid@gmail.com>
2011-08-24 03:26:12 -07:00
David Aguilar cc45442862 views.dag: Set splitter heights to 2/3 graph, 1/3 controls
Signed-off-by: David Aguilar <davvid@gmail.com>
2011-08-24 03:23:10 -07:00
David Aguilar 4824c48b8d views.dag: Add 'thirdparty' to sys.path when running standalone
Signed-off-by: David Aguilar <davvid@gmail.com>
2011-08-24 03:22:27 -07:00
David Aguilar ba60e451d9 qtutils: Add an add_action() helper for creating QActions
Signed-off-by: David Aguilar <davvid@gmail.com>
2011-08-24 03:20:09 -07:00
David Aguilar c918b77fd7 views.dag: Use generation numbers when drawing the DAG
Use prettier colors when drawing the DAG.

Adjust item sizes to make things fit together more closely.

Use class attributes to avoid creating creating colors in every
instance of the Node class.

Use a simple calculation based on a commit's generation number
to determine its (x, y) location in the scene.

Signed-off-by: David Aguilar <davvid@gmail.com>
2011-08-23 13:56:00 -07:00
David Aguilar 2f30c346b3 models.dag: Keep track of the generation number for commits
The generation number is a robust and simple property we can use
when drawing the DAG.  Change the "git log" command so that it
lists commits in reverse-topological order.  This allows us to
track generation numbers while building the graph incrementally.

Signed-off-by: David Aguilar <davvid@gmail.com>
2011-08-23 13:56:00 -07:00
David Aguilar 5d4d43dcff views.dag: Do not keep references to GitDAGWidgets
Signed-off-by: David Aguilar <davvid@gmail.com>
2011-08-23 13:56:00 -07:00
David Aguilar 3e96c6db2b views.dag: Tighten up the DAG widget's layout
The splitter widgets are now collapsible.
The DAG graphics view's default size is now larger.

Signed-off-by: David Aguilar <davvid@gmail.com>
2011-08-23 13:56:00 -07:00
David Aguilar 120b4edf0f views.main: Pass a parent widget to the DAG view
Signed-off-by: David Aguilar <davvid@gmail.com>
2011-08-23 13:56:00 -07:00
David Aguilar d0388c6e65 setup.py: Try git when printing the release version
Signed-off-by: David Aguilar <davvid@gmail.com>
2011-08-20 19:50:40 -07:00
David Aguilar 39302a13ec doc: Add 1.4.3.5 release notes
Signed-off-by: David Aguilar <davvid@gmail.com>
2011-08-20 19:40:56 -07:00
David Aguilar e281c208ab Makefile: Cleanup stray files in $prefix/lib*
Sometimes python's LIBDIR is not the same as the lib/ directory used by
distutils so change the Makefile so that it uses lib*.

Signed-off-by: David Aguilar <davvid@gmail.com>
2011-08-19 12:25:54 -07:00
David Aguilar 46f7530225 doc: Add Karl B. to the "Thanks To" page
Signed-off-by: David Aguilar <davvid@gmail.com>
2011-08-16 20:31:00 -07:00
David Aguilar b367a90a94 Merge pull request #87 from kbielefe/inotify-enhancements
Inotify enhancements
2011-08-16 20:29:05 -07:00
Karl Bielefeldt a9356a41a3 Add Windows support for inotify events 2011-08-16 13:10:13 -05:00
Karl Bielefeldt dfa8841fd8 Use filenames to speed up inotify rescans 2011-08-16 13:06:16 -05:00
David Aguilar 3a459d32e3 models.gitrepo: Add a task manager class to avoid PyQt segfaults
The task manager holds references to individual tasks to prevent Python's
garbage collector from reaping running tasks, which causes a segfault.

Signed-off-by: David Aguilar <davvid@gmail.com>
2011-08-09 15:58:14 -07:00
David Aguilar 996189613f models.gitrepo: Add some micro-optimizations
GitRepoModel._create_dir_entry() gets called many times for large repos so avoid
expensive "." lookups where possible.

GitEntryManager.entry() now uses a local reference to its instance
dictionary to avoid doing extra Python name lookups.

Signed-off-by: David Aguilar <davvid@gmail.com>
2011-08-09 15:57:54 -07:00
David Aguilar 039b4b2ed6 models.gitrepo: Fix typo calling the wrong base class in GitRepoModel.__init__
Signed-off-by: David Aguilar <davvid@gmail.com>
2011-08-09 15:56:58 -07:00
David Aguilar ca9dfb8e3e remote: Improve user interaction for remote actions
Make sure the 'remote' section is always at the top and focused.
When doing 'pull', do not select a local branch by default.

This ensures that we do "git pull $remote" instead of
"git pull $remote $branch", which only updates the local
branch and leaves the remote tracking branch behind.

Signed-off-by: David Aguilar <davvid@gmail.com>
2011-08-09 15:56:42 -07:00
David Aguilar 844f5c1350 views.main: Improve the user interaction when committing with no changes
Change the "Yes" / "Cancel" confirmation buttons to say
"Stage and Commit" / "Cancel", which is clearer and more user-friendly.

Signed-off-by: David Aguilar <davvid@gmail.com>
2011-08-09 15:55:53 -07:00
David Aguilar 79cf462dbe Makefile: Rework variables for better readability
Variables that are externally defined should use ALL_CAPS.
Internal variables should use small_caps.

The net effect is that command names and DESTDIR are the
only capitalized variables, which is simple and consistent.

Signed-off-by: David Aguilar <davvid@gmail.com>
2011-08-08 03:24:27 -07:00
David Aguilar d8525e47b9 cola.version: Remove regular expressions
Replace usage of the 're' module with simpler but equivalent
string checks.

Signed-off-by: David Aguilar <davvid@gmail.com>
2011-08-08 03:17:31 -07:00
David Aguilar dbfd93a5c3 Makefile: Better support for out-of-git builds
The top-level Makefile depended on the existence of cola's
git repository to generate its version number.

Rework the Makefile so that the "make dist"-generated "version"
file is used instead.  This is done by scraping the output of
"python cola/version.py", which knows to look for the "version"
file before trying to use "git describe".

Signed-off-by: David Aguilar <davvid@gmail.com>
2011-08-07 22:45:19 -07:00
David Aguilar 1cd1672276 style: Remove unused imports and other fixes
Signed-off-by: David Aguilar <davvid@gmail.com>
2011-08-01 10:38:40 -07:00
David Aguilar 44fc1dbf29 models.main: Tighten-up the required params for config_set()
Signed-off-by: David Aguilar <davvid@gmail.com>
2011-07-30 02:12:57 -07:00
David Aguilar d4be2558ba tests: Update settings test case to remove module global
Signed-off-by: David Aguilar <davvid@gmail.com>
2011-07-30 02:11:04 -07:00
David Aguilar 22bdc9d1aa models.main: Remove unused vars
Signed-off-by: David Aguilar <davvid@gmail.com>
2011-07-30 01:49:45 -07:00
David Aguilar 6da5dc666f Makefile: Update 'tags' target to use find and xargs
Signed-off-by: David Aguilar <davvid@gmail.com>
2011-07-29 17:31:02 -07:00
David Aguilar 6b40eed965 cmds: Log stderr when applying partial diffs
stderr was not getting captured which meant that output from 'git apply'
was going to the shell.  Capture stderr and log it instead.

Signed-off-by: David Aguilar <davvid@gmail.com>
2011-07-29 16:34:50 -07:00
David Aguilar 3d14ec1a36 remote: Use the phrase "Transferring" instead of "Connecting"
"Connecting" makes users think that we're trying to
establish a connection.  We're really transferring data
to/from the remote so use that verb instead.

Signed-off-by: David Aguilar <davvid@gmail.com>
2011-07-15 17:07:05 -07:00
David Aguilar 3bbc82dd6e settings: Do not 'import user'
The use of import user in settings.py causes anything in a user's
PYTHONSTARTUP script to execute.  This can have unexpected side effects.

Use os.path.expanduser('~') instead of user.home.

Closes #86

Signed-off-by: David Aguilar <davvid@gmail.com>
2011-07-15 17:02:03 -07:00
David Aguilar c4d05efdfe dag: Avoid embedding NULL in git output
Newer version of msysgit do not allow NULL in git's output so
use byte 0x01 as our separator instead.

Closes #85

Signed-off-by: David Aguilar <davvid@gmail.com>
2011-07-01 02:43:37 -07:00
David Aguilar cff51b2ff2 classic: Avoid embedding NULL in git output
Newer version of msysgit do not allow NULL in git's output so
use byte 0x01 as our separator instead.

Signed-off-by: David Aguilar <davvid@gmail.com>
2011-07-01 02:42:58 -07:00
David Aguilar 9ebdfd942f gitcmds: Use --pretty=format:... for older gits
Older versions of git do not understand --format=...
so use --pretty=format:... instead.

Signed-off-by: David Aguilar <davvid@gmail.com>
2011-07-01 02:13:17 -07:00
David Aguilar 9218e16272 cmds: Handle unicode in the UntrackedSummary command
Signed-off-by: David Aguilar <davvid@gmail.com>
2011-06-30 20:17:33 -07:00
David Aguilar 7ff1bbe966 qtutils: Use selectedItems() instead of currentRow()
Signed-off-by: David Aguilar <davvid@gmail.com>
2011-06-30 20:12:33 -07:00
David Aguilar 3f136a496b push: Honor remote.$remote.pushurl
"git push" uses remote.$remote.pushurl when configured so follow
the same rules when displaying the remotes.

Closes #83

Signed-off-by: David Aguilar <davvid@gmail.com>
2011-06-30 20:04:12 -07:00
David Aguilar 3d6e63b403 status: Anchor new .gitignore entries to the root
Adding "foo" to .gitignore should use "/" to anchor it to the root.

Signed-off-by: David Aguilar <davvid@gmail.com>
2011-06-30 03:25:38 -07:00
David Aguilar 134c70f756 Add Audrius to the thanks page
Signed-off-by: David Aguilar <davvid@gmail.com>
2011-06-30 03:22:49 -07:00
David Aguilar 7e7abcd058 Merge pull request #84 from shivas/issue59
.gitignore: Adds additional right-click command for untracked files to add them to .gitignore
2011-06-30 03:09:29 -07:00
Audrius Karabanovas df904f0a18 .gitignore: Adds additional right-click command for untracked files to add them to .gitignore file in repository
If .gitignore file isn't used in repository, it's created as new file and needs to be added to repository manualy

solution for issue #59 at https://github.com/davvid/git-cola/issues/59
2011-06-29 23:06:41 +04:00
David Aguilar 246060e74f perf: Avoid calling 'git update-index --refresh' too often
We now call 'git update-index --refresh' on startup and in the 'Refresh'
command only.

The other commands call model.update_file_status() which avoids
updating the entire index, updating remotes, etc.

This shaves off a few unneeded git calls.  The stage/unstage methods
previously included some performance hacks to avoid needing to rescan
for changes.  These have been removed in favor of the simpler "just rescan"
logic.  This will become less of an issue in the future when we do all
rescanning in a background thread.

An important note is that all of the editor commands call update_status()
explicitly instead of relying on the base class to call it for them.
This is better since it makes things more explicit.

Signed-off-by: David Aguilar <davvid@gmail.com>
2011-06-18 04:39:48 -07:00
David Aguilar 43aacbc260 gitcmds: Fix unstaging of files when amending a commit
Pass an explicit head when unstaging files with 'git reset'.
This makes unstaging files from the previous commit work correctly
since we do not always want to reset to the contents of 'HEAD'.

The net effect is that we now call 'git reset HEAD -- <paths>' for the
normal case and 'git reset HEAD~ -- <paths>' when amending a commit.

Closes #82

Signed-off-by: David Aguilar <davvid@gmail.com>
2011-06-18 03:45:22 -07:00
David Aguilar 37b5ed08ef remote: Show the right remote name when push fails
Signed-off-by: David Aguilar <davvid@gmail.com>
2011-06-01 14:33:26 -07:00
David Aguilar 3b936753fc stash: Add an icon to the 'Close' button
Signed-off-by: David Aguilar <davvid@gmail.com>
2011-05-27 13:43:27 -07:00
David Aguilar 746dcea928 stash: Parent the dialog so that it is centered
Signed-off-by: David Aguilar <davvid@gmail.com>
2011-05-27 13:43:27 -07:00
David Aguilar 62c60587e6 remote: Rename 'Cancel' to 'Close' and add icons to the buttons
Signed-off-by: David Aguilar <davvid@gmail.com>
2011-05-27 13:43:27 -07:00
David Aguilar eb98e69a8c remote: Parent the information dialog so that it is centered
Add a 'parent' flag to qtutils.information() so that we can
parent the message box to the specified parent.
This makes the dialog appear centered relative to the parent.

Signed-off-by: David Aguilar <davvid@gmail.com>
2011-05-27 13:42:34 -07:00
David Aguilar 094b3c5279 remote: Display a completion dialog at the end of fetch/push/pull
Signed-off-by: David Aguilar <davvid@gmail.com>
2011-05-27 01:50:39 -07:00
David Aguilar c85dc03e72 views.remote: Use a better default size
Signed-off-by: David Aguilar <davvid@gmail.com>
2011-05-27 01:35:54 -07:00
David Aguilar eb3a1adb50 qtutils: Make information() prettier and more user-friendly
This adds the ability to provide informative and details text in the
information() helper.  A better icon is used as well.

Signed-off-by: David Aguilar <davvid@gmail.com>
2011-05-27 01:33:38 -07:00
David Aguilar da8eb59f66 status: Make the tree animated and use uniform row heights for performance
Signed-off-by: David Aguilar <davvid@gmail.com>
2011-05-24 03:33:08 -07:00
David Aguilar 63fb34ce4f mainwindow: Make 'Signoff' and 'Commit' tool buttons with icons
Signed-off-by: David Aguilar <davvid@gmail.com>
2011-05-24 03:19:34 -07:00
David Aguilar 81eb60dd97 mainwindow: Center the position label
Signed-off-by: David Aguilar <davvid@gmail.com>
2011-05-24 03:19:34 -07:00
David Aguilar 727c8df183 views.stash: Use qt.create_toolbutton() instead of duplicating effort
Signed-off-by: David Aguilar <davvid@gmail.com>
2011-05-24 03:19:34 -07:00
David Aguilar 9b07159568 qt; Add create_toolbutton() and make create_button() consistent
Signed-off-by: David Aguilar <davvid@gmail.com>
2011-05-24 03:19:34 -07:00
David Aguilar 3faaa3da0e mainwindow: Remove 'Search' menu
The 'Search' menu actions moved into Actions->Grep and Commit->Search.
This eliminates an entire menu, which is good.

Signed-off-by: David Aguilar <davvid@gmail.com>
2011-05-24 02:50:38 -07:00
David Aguilar 7379783ec7 search: Add better support for paths with spaces, etc.
Use the symmetrical subprocess.list2cmdline() and shlex.split()
to better support paths with spaces.

Signed-off-by: David Aguilar <davvid@gmail.com>
2011-05-24 02:49:02 -07:00
David Aguilar 1de012f8b7 merge: Use qtutils.confirm() when asking whether to abort a merge
Signed-off-by: David Aguilar <davvid@gmail.com>
2011-05-24 02:08:21 -07:00
David Aguilar 538a3b1583 qtutils: Translate all strings in confirm()
Signed-off-by: David Aguilar <davvid@gmail.com>
2011-05-24 02:08:21 -07:00
David Aguilar 6c3913aaef i18n: Set text domain and setup environ in i18n.install()
Signed-off-by: David Aguilar <davvid@gmail.com>
2011-05-24 01:30:08 -07:00
David Aguilar 010440a47c qtutils: Add close_icon() and more docstrings
Signed-off-by: David Aguilar <davvid@gmail.com>
2011-05-23 20:26:55 -07:00
David Aguilar aadbc8541a qtutils: Add icons for Save, Apply, Discard, and Ok
Signed-off-by: David Aguilar <davvid@gmail.com>
2011-05-23 09:39:47 -07:00
David Aguilar 7b557b2fa2 stash: Rework the entire stash interaction to be more intuitive
We now disable unavailable actions, have a much nicer UI,
and provide prompts with more intuitive interfaces.

Death to the old stash dialog!

Signed-off-by: David Aguilar <davvid@gmail.com>
2011-05-22 23:53:29 -07:00
David Aguilar a723a81079 qtutils: Add a 'confirm' function for better user interrogation
question() asks Yes/No questions which are not very user-friendly.
Dialog buttons should say things like 'Save', not 'Yes'.

confirm() allows the caller to supply the text and icon for the
confirmation button so that the interaction can be improved.

Signed-off-by: David Aguilar <davvid@gmail.com>
2011-05-22 23:52:05 -07:00
David Aguilar 59f9a4639a qtutils: Adjust whitespace
Signed-off-by: David Aguilar <davvid@gmail.com>
2011-05-22 23:37:34 -07:00
David Aguilar 2455bb831a qtutils: Make selected_item() operate on the selection, not the curren item
Signed-off-by: David Aguilar <davvid@gmail.com>
2011-05-22 23:37:06 -07:00
David Aguilar 23832e6c5a models.main: Add ability to parse the 'name' portion of a stash
Signed-off-by: David Aguilar <davvid@gmail.com>
2011-05-22 23:35:32 -07:00
David Aguilar 6324036319 qobserver: Register clicked() signal for buttons instead of release()
Registering on release() does not allow us to click, hold, and drag away
to prevent the action from triggering.  clicked() is better because what
we really want to know is whether we were clicked, not whether the
release event arrived.

Signed-off-by: David Aguilar <davvid@gmail.com>
2011-05-22 22:11:51 -07:00
David Aguilar fdbce9b00d stash: Rescan after applying and saving stashes
Signed-off-by: David Aguilar <davvid@gmail.com>
2011-05-22 21:12:41 -07:00
David Aguilar b08e125c87 stash: Use a nicer layout in the stash GUI
Signed-off-by: David Aguilar <davvid@gmail.com>
2011-05-22 08:44:45 -07:00
David Aguilar dca01509c5 mainwidnow: Simplify the commit message editor layouts
Remove an unnecessary layout and use consistent spacing around the buttons.

Signed-off-by: David Aguilar <davvid@gmail.com>
2011-05-22 08:33:54 -07:00
David Aguilar e373d27e75 status: Make top-level items hidden by default
The status widget is constructed before its state is set so sometimes
cola may appear with its four top-level "Staged", "Unstaged",
"Unmerged", and "Untracked" items in an empty state before being
immediatley updated.

Avoid this by hiding the top-level items on first construction so that
they only appear later when the widget is populated.

Signed-off-by: David Aguilar <davvid@gmail.com>
2011-05-22 08:21:57 -07:00
David Aguilar 33fdd38b11 status: Remove the global status instance
Remove the global singleton since we don't really need it.
Create a normal instance in the main window instead.

Signed-off-by: David Aguilar <davvid@gmail.com>
2011-05-22 08:14:46 -07:00
David Aguilar 352767fa3a remote: Fix animation on Mac OS X by enabling the progress dialog
Disabling the parent widget disables the progress dialog on Mac OS X.
Work around this by explicitly enabling the progress dialog
after disabling the parent dialog.

Signed-off-by: David Aguilar <davvid@gmail.com>
2011-05-20 03:59:32 -07:00
David Aguilar 5ab9d12f90 remote: Use a background thread when calling git fetch/push/pull
This fixes the progress bar behavior on Linux by using a QRunnable
to handle the heavy lifting.

Signed-off-by: David Aguilar <davvid@gmail.com>
2011-05-20 04:16:17 -07:00
David Aguilar 5d8dcf9e18 mainwindow: Tighten the layouts for diff and log widgets
Signed-off-by: David Aguilar <davvid@gmail.com>
2011-05-20 03:05:51 -07:00
David Aguilar de751a993e ux: Make the 'Merge' dialog modal
Signed-off-by: David Aguilar <davvid@gmail.com>
2011-05-20 00:31:04 -07:00
David Aguilar c667500d30 ux: Make the 'Create Tag' dialog modal
Signed-off-by: David Aguilar <davvid@gmail.com>
2011-05-20 00:30:33 -07:00
David Aguilar ad6e151d48 stash: Make the dialog modal and use a simpler layout
Signed-off-by: David Aguilar <davvid@gmail.com>
2011-05-20 00:30:09 -07:00
David Aguilar 450b92715b Update copyright for 2011
Heh.

Signed-off-by: David Aguilar <davvid@gmail.com>
2011-05-20 00:13:17 -07:00
David Aguilar 1bc259730f doc: Add 1.4.3.4 release notes
Signed-off-by: David Aguilar <davvid@gmail.com>
2011-05-20 00:12:26 -07:00
David Aguilar 35291f00eb doc: Add "Thank to Christian Jann and Justin Lecher"
Christian really helped round out our unicode handling
Justin fixed cloning in the --prompt dialog.

Signed-off-by: David Aguilar <davvid@gmail.com>
2011-05-20 00:10:22 -07:00
David Aguilar baad1d4496 remote: Disable the dialog and show a progress bar in fetch/push/pull
This makes the interaction much nicer by giving feedback during the
remote fetch, push, and pull operations.

Signed-off-by: David Aguilar <davvid@gmail.com>
2011-05-19 23:57:20 -07:00
David Aguilar 1580666545 rmeote: Make the 'remote' dialogs modal to the current window
This makes the remote dialogs drop down in the context of the
main window.  It also adds a cool animation.

Signed-off-by: David Aguilar <davvid@gmail.com>
2011-05-19 23:56:13 -07:00
David Aguilar c19f2fdd4e ux: Remove syntax highlighting from the 'Command Output' window
The syntax highlighting was not very useful and could sometimes make
things harder to read.

Signed-off-by: David Aguilar <davvid@gmail.com>
2011-05-19 22:47:56 -07:00
David Aguilar 8124f27e19 remote: Show an error message when 'Push' fails
"git push" might fail but we provide no indication of this to
the user.  Show a helpful error message when this happens.

Closes #69

Signed-off-by: David Aguilar <davvid@gmail.com>
2011-05-19 22:43:23 -07:00
David Aguilar f849eee3c7 gitcmds: Split lines after unicode decoding
We were decoding the output of splitlines which is completely wrong.

Signed-off-by: David Aguilar <davvid@gmail.com>
2011-05-19 22:43:23 -07:00
David Aguilar 62359adc34 mainwindow: Tighten the message editor layout
Signed-off-by: David Aguilar <davvid@gmail.com>
2011-04-29 01:25:35 -07:00
David Aguilar e1175c3761 mainwindow: Replace spacers with addStretch() calls
Signed-off-by: David Aguilar <davvid@gmail.com>
2011-04-29 01:18:42 -07:00
David Aguilar 066fd281db ux: Rename "Options" to "Preferences"
Signed-off-by: David Aguilar <davvid@gmail.com>
2011-04-29 01:15:37 -07:00
David Aguilar 2012d6c8d3 Merge branch 'jl/clone-with-prompt'
* jl/clone-with-prompt:
  Fix for issue 76, Clone doesn't work with --prompt.
2011-04-26 01:29:38 -07:00
David Aguilar 7101cb8303 gitcmds: Support unicode in all_refs()
This allows us to have unicode branches and tags.

Closes #79

Signed-off-by: David Aguilar <davvid@gmail.com>
2011-04-26 01:27:03 -07:00
David Aguilar 560ae38f68 views.combo: Handle unicode content
Porting cola to python3k will probably require a cola.compat
variable where we set unicode = str.  Or not.  It'd be nice
to support both 2 and 3 with the same codebase, but maybe
that's crazy.  :-/

Signed-off-by: David Aguilar <davvid@gmail.com>
2011-04-26 01:27:03 -07:00
David Aguilar 75e7315cb8 gitcmds: Handle unicode in for_each_basename()
Signed-off-by: David Aguilar <davvid@gmail.com>
2011-04-26 01:27:03 -07:00
David Aguilar 2fbccecc0d gitcmds: Handle unicode in current_branch()
Signed-off-by: David Aguilar <davvid@gmail.com>
2011-04-26 01:27:03 -07:00
David Aguilar 3361761bc3 views.main: Properly decode the path to the merge message
The path to .git/MERGE_MSG may contain unicode characters so decode
it before trying to read the file.

Closes #74

Signed-off-by: David Aguilar <davvid@gmail.com>
2011-04-23 14:22:15 -07:00
David Aguilar 1645870bb4 gitcmds: Support unicode in repository paths for the "git init" case
We were not properly handling repositories with unicode in
their paths for the "git init" case.  We read .git/HEAD directly
and thus need to decode the path to .git/HEAD since its parent
directory may contain unicode characters.

Closes #74

Signed-off-by: David Aguilar <davvid@gmail.com>
2011-04-23 12:11:13 -07:00
David Aguilar ada59fa952 doc: Document the --prompt option and fix a typo.
Signed-off-by: David Aguilar <davvid@gmail.com>
2011-04-23 00:21:14 -07:00
David Aguilar 2989422aee app: Use four-space indents in instance()
Signed-off-by: David Aguilar <davvid@gmail.com>
2011-04-23 00:20:48 -07:00
David Aguilar 8c1d1c8f05 doc: Add 1.4.3.3 release notes
Signed-off-by: David Aguilar <davvid@gmail.com>
2011-04-22 20:21:20 -07:00
David Aguilar 7cf0246a14 startup: Fix the 'clone' button in the dialog
We removed the Qt 'parent' field so stop providing it in
the startup dialog.

Fixes: #76

Signed-off-by: David Aguilar <davvid@gmail.com>
2011-04-22 20:12:44 -07:00
Justin Lecher 623b809b8a Fix for issue 76, Clone doesn't work with --prompt.
Signed-off-by: Justin Lecher <jlec@gentoo.org>
2011-04-16 11:45:12 +02:00
David Aguilar 9b666a5e03 main: Add a '--prompt' option and use it in the desktop launchers
git-cola assumes that the current directory is a git repository.
This is less helpful when launching git-cola through a launcher icon
because it may find a git repository in $HOME.

Add a '--prompt' flag and use it in all of the launchers.
This forces git-cola to prompt the user for a repository before
launching the main GUI.

Closes #75

Signed-off-by: David Aguilar <davvid@gmail.com>
2011-04-15 02:11:01 -07:00
David Aguilar 1db197ca4d bookmarks: Pass '--repo' when launching git-cola on a bookmark
Otherwise git-cola launches in-place when opening a bookmark.

Closes #75

Signed-off-by: David Aguilar <davvid@gmail.com>
2011-04-15 01:58:51 -07:00
David Aguilar 73a7191758 startup: Handle repository paths with unicode characters
Closes #74

Signed-off-by: David Aguilar <davvid@gmail.com>
2011-04-15 01:40:07 -07:00
David Aguilar 00f6b77ee1 settings: Handle home directories with unicode characters
Closes #74

Signed-off-by: David Aguilar <davvid@gmail.com>
2011-04-15 01:38:46 -07:00
David Aguilar bc5940bd36 guicmds: Handle cloning repos that contain unicode characters
Closes #74

Signed-off-by: David Aguilar <davvid@gmail.com>
2011-04-15 01:38:00 -07:00
David Aguilar 6a09349645 bookmarks: Handle home directories with unicode characters
Closes #74

Signed-off-by: David Aguilar <davvid@gmail.com>
2011-04-15 01:37:14 -07:00
David Aguilar 297ad386e3 INSTALL: Fix mistyped win32 installer path
Signed-off-by: David Aguilar <davvid@gmail.com>
2011-04-13 02:38:57 -07:00
David Aguilar 920f736b6e doc: Add 1.4.3.2 release notes draft
Signed-off-by: David Aguilar <davvid@gmail.com>
2011-04-10 03:59:38 -07:00
David Aguilar 3ee70d6800 gitcfg: Properly decode unicode in file paths
Both the user's home directory and the repository directory can
contain unicode characters in their path.  Decode inputs from
the filesystem so that these paths are accepted.

Closes #74

Signed-off-by: David Aguilar <davvid@gmail.com>
2011-04-10 03:43:10 -07:00
David Aguilar 2975050c94 utils: Remove unused 'commands' import
We needed 'commands' back when we built our own strings using
'commands.mkarg()'.  We now use subprocess.list2cmdline() instead.

Signed-off-by: David Aguilar <davvid@gmail.com>
2011-03-19 20:06:56 -07:00
David Aguilar a2e46a327f utils: Fix launching background commands on windows
Newer versions of msysgit no longer require the win32_expandpaths()
hack to expand paths after the double-dash.

Remove this old hack to make newer gits happy.

This basically means we don't support the older msysgit which
required the workaround, which is fine.  If we find someone is
still using an old version we can always add a check later.

Signed-off-by: David Aguilar <davvid@gmail.com>
2011-03-19 19:30:48 -07:00
David Aguilar e823d670cc utils: Use subprocess.list2cmdline() to launch commands in the background
This is probably safer then relying on commands.mkarg().

Signed-off-by: David Aguilar <davvid@gmail.com>
2011-03-19 19:17:38 -07:00
David Aguilar bd7f59bc05 cmds: Use sys.executable instead of 'python'
Do not require 'python' in the $PATH by using sys.executable.

Signed-off-by: David Aguilar <davvid@gmail.com>
2011-03-19 19:15:14 -07:00
David Aguilar 55a71ee4ab views.main: Hold onto background tasks to avoid segfaults on older PyQt versions
PyQt 4.6.2 segfaults if we don't hold onto a reference to our QRunnable tasks.
Python assumes they are unreferenced and safe to garbage collect.

Keep a reference to the task objects to prevent Python from garbage
collecting them.

Signed-off-by: David Aguilar <davvid@gmail.com>
2011-03-10 16:14:33 -08:00
David Aguilar cec9bbaef0 main: Read initial git state in a background thread
Use QThreadPool to offload several git operations so that
git-cola starts up faster.  We can later look into using
'git status --porcelain' to eliminate a few more git calls.

Signed-off-by: David Aguilar <davvid@gmail.com>
2011-03-10 00:38:55 -08:00
David Aguilar ab29466607 views.main: Load GUI state in a background thread
Every little bit helps when it comes to startup time.

Signed-off-by: David Aguilar <davvid@gmail.com>
2011-03-10 00:38:55 -08:00
David Aguilar 36fece9243 views.main: Load config actions in a background thread
Loading configured actions can be expensive so do it in the background.
The actions module now returns config action names instead of doing
the menu updating itself.  When QThreadPool is available we listen
for ''install_config_actions' and add callbacks for each name
sent to it.

Signed-off-by: David Aguilar <davvid@gmail.com>
2011-03-10 00:38:55 -08:00
David Aguilar e15237e62b views: Make observer callbacks thread-safe
Prepare to offload expensive operations onto a background thread
by making all of the observer-registered GUI callbacks thread-safe.
We do this by using Qt's signal/slots.  When a callback happens in
another thread it simply posts a signal using emit().

The main (GUI) thread registers for these signals and handles
updating the views in response to the signals.

Signed-off-by: David Aguilar <davvid@gmail.com>
2011-03-10 00:38:55 -08:00
David Aguilar 2ae4777638 gitcfg: Micro-optimize the stat cache
Signed-off-by: David Aguilar <davvid@gmail.com>
2011-03-10 00:38:55 -08:00
David Aguilar ee150af446 difftool: Add support for p4merge
Signed-off-by: David Aguilar <davvid@gmail.com>
2011-03-10 00:38:55 -08:00
David Aguilar d32d2bea41 difftool: Use read-only mode when using *vimdiff
Signed-off-by: David Aguilar <davvid@gmail.com>
2011-03-10 00:38:55 -08:00
David Aguilar f54b479f47 difftool: Use "compare" to launch Araxis merge
Signed-off-by: David Aguilar <davvid@gmail.com>
2011-03-10 00:38:55 -08:00
David Aguilar 106cddcef6 difftool: Specialize diff options for emerge and ecmerge
The ecmerge documentation mentions the following form:

	ecmerge --mode=diff2 $1 $2

Since git-difftool is about diffing, we should use that instead
of --mode=merge2.  Likewise, this drops the $MERGED argument to
emerge, as discussed on the git list ($gmane/117930).

Signed-off-by: David Aguilar <davvid@gmail.com>
2011-03-10 00:38:55 -08:00
David Aguilar 5260e0f067 tests: Fix the gitcmds test case
Reset the cache where appropriate.

Signed-off-by: David Aguilar <davvid@gmail.com>
2011-03-10 00:38:55 -08:00
David Aguilar 5af01cdbbf doc: Add 1.4.3.1 release notes
Signed-off-by: David Aguilar <davvid@gmail.com>
2011-02-27 00:02:01 -08:00
David Aguilar 792148e069 doc: Add "Kevin K" to the "Thanks to" page
He's provided the Fedora translations patch which
was applied in 1aaeccd369.

Signed-off-by: David Aguilar <davvid@gmail.com>
2011-02-27 00:01:40 -08:00
David Aguilar f9bb2c1407 cmds: Use 'UsageError' in the commit template command objects
Add error checking so that we can detect when users provide either
bad paths or unconfigured commit message templates.

Previously, an unconfigured "commit.template" git config variable
would result in a stacktrace.  We now present users with a helpful
error message instead.

Closes #72

Signed-off-by: David Aguilar <davvid@gmail.com>
2011-02-26 23:38:26 -08:00
David Aguilar 68e464c8f0 cmdfactory: Catch 'UsageErrors' and provide useful feedback
Prompt users when a 'UsageError' occurs.  We currently use the simple
'information' callback.  Different error classes can define fancier
ones later as needed.

Signed-off-by: David Aguilar <davvid@gmail.com>
2011-02-26 23:30:25 -08:00
David Aguilar 814227f984 errors: Add 'UsageError' for use by command objects
UsageError represents usage errors.

Signed-off-by: David Aguilar <davvid@gmail.com>
2011-02-26 23:29:01 -08:00
David Aguilar 84e1922b4d inotify: Rework thread timeouts to consume less resources
Register known paths up front to avoid checks within a loop.
Updates timeouts so that we sleep for slightly longer.
We no longer use quit() when ending the inotify thread.
We call stop() and wait for it to complete.  This allows us
to do without the setTerminationEnabled(True) call,
which is generally discouraged.

Signed-off-by: David Aguilar <davvid@gmail.com>
2011-02-02 02:24:20 -08:00
David Aguilar 107b336826 inotify: Watch for IN_CLOSE_WRITE instead of IN_CREATE
Signed-off-by: David Aguilar <davvid@gmail.com>
2011-02-02 02:24:20 -08:00
David Aguilar 61cc4575a6 inotify: Do not hide the 'Rescan' button when inotify is enabled
Some people find it useful.

Signed-off-by: David Aguilar <davvid@gmail.com>
2011-02-02 02:24:20 -08:00
David Aguilar 8fb8a27fa0 views.main: Always call keyPressEvent() in the diff view
Ensure that we don't interfere with Qt's event processing by
unconditionally calling the default keyPressEvent() in our
custom callback.

Signed-off-by: David Aguilar <davvid@gmail.com>
2011-02-02 02:23:08 -08:00
David Aguilar 091ffcd496 views.about: Decrease font size and reformat "about" text
Avoid wrapping lines by reflowing the copyright paragraph.

Signed-off-by: David Aguilar <davvid@gmail.com>
2011-01-24 18:30:30 -08:00
David Aguilar 986e205e76 classic: Redo startup initialization for standalone mode
The gitrepo QStandardItemModel checks fields in the model which were not
initialized in the 'git cola --classic' standalone case.
Scan the repository before initializing any GUIs so that all of the
model data is available beforehand.

Signed-off-by: David Aguilar <davvid@gmail.com>
2011-01-20 03:19:19 -08:00
David Aguilar d6f9095b98 Makefile: Rename Darwin targets to make more sense
Users might want to 'make git-cola.app' and have it DTRT instead of
giving them a tarball.

Signed-off-by: David Aguilar <davvid@gmail.com>
2011-01-20 03:00:06 -08:00
David Aguilar b1dad205d7 classic: Rework the status column to use Uri Okrent's snazzy sigil icons
QStandardItem only lets us set a single icon which is good enough for now.
The 'Untracked' text was made less cluttered by simply showing a '?' instead.
That way only the statuses with icons have text next to them.

Signed-off-by: David Aguilar <davvid@gmail.com>
2011-01-20 02:52:25 -08:00
David Aguilar 44b816381a classic: Add icons for staged, modified, etc.
Funky fresh icons for the classic view courtesy of Uri Okrent.

Signed-off-by: David Aguilar <davvid@gmail.com>
2011-01-20 02:52:18 -08:00
David Aguilar 1736682a77 Revert "models: Handle adding .gitignore'd files"
This reverts commit 16e5126461.

When staging an upper-level folder using 'cola classic'
in ends up staging all the untracked files in that subdirectory.
Oops!  Adding ignored files is an edge case and we don't need
to support it if it hurts the common case.

Signed-off-by: David Aguilar <davvid@gmail.com>
2011-01-20 02:49:10 -08:00
David Aguilar d393307727 Makefile: Separate Darwin app bundle and tarball targets
This lets us "make app-bundle" to get an untarred git-cola.app.

Signed-off-by: David Aguilar <davvid@gmail.com>
2011-01-20 01:38:08 -08:00
David Aguilar dbb89e54fe gitcfg: Fix 'git config' caching
Re-initialize the data dictionaries directly instead of
calling reset().  This fixes git config caching since
the cache data isn't continually discarded.

Signed-off-by: David Aguilar <davvid@gmail.com>
2011-01-15 09:03:38 -08:00
David Aguilar 18c14746dc view.main: Simplify 'Signoff' and 'Commit' callbacks
Remove the unnecessary editor command for the 'signoff' action
so that the Qt widget can do the heavy lifting.

Signed-off-by: David Aguilar <davvid@gmail.com>
2011-01-15 07:44:12 -08:00
David Aguilar 1aaeccd369 setup.py: Fix installation of translations per Fedora
Incorporate Fedora's fix for the translations path.
This originally appeared in cola-1.4.3-translations.patch.

Patched-by: Kevin Kofler <Kevin@tigcc.ticalc.org>
Signed-off-by: David Aguilar <davvid@gmail.com>
2011-01-12 19:26:43 -08:00
David Aguilar 16e5126461 models: Handle adding .gitignore'd files
If a file is .gitignore'd we are unable to add it because we are using
'git add' as-is.  Pass '--force' so that we can stage files regardless
of whether they exist in .gitignore or not.

Signed-off-by: David Aguilar <davvid@gmail.com>
2011-01-12 19:13:25 -08:00
David Aguilar 6aa01795e0 models: Handle staging an arbitrary number of files
Slice the arguments passed on the command line so that staging a large
list of files does not error out with 'errno: Argument list too long'.

We do this by incrementally staging 42 files at a time.

Signed-off-by: David Aguilar <davvid@gmail.com>
2011-01-12 19:12:36 -08:00
David Aguilar 18100eb677 win32/cola: Unify style and guard against spaces in Python paths
Users might have Python installed in a path with a space in it
so guard against this possibility even if we don't check any
of those paths ourselves.

Signed-off-by: David Aguilar <davvid@gmail.com>
2011-01-02 18:17:03 -08:00
David Aguilar a75b9e111d views.startup: Raise the widget only after showing it
Qt on OS X doesn't honor raise_() unless show() has been
called so swap the order.

Signed-off-by: David Aguilar <davvid@gmail.com>
2011-01-02 18:05:32 -08:00
David Aguilar 05364663e4 views.mainwindow: Make the 'Cola Classic' tool dockable
This adds the ability to have a dockable 'Cola Classic' tool.
This is enabled by setting the 'cola.classicdockable' configuration.  e.g.:

	git config cola.classicdockable true

The default is 'false' simply because it slows the down startup time
and is especially noticable on large repositories.

Closes #56

Signed-off-by: David Aguilar <davvid@gmail.com>
2010-12-28 03:02:58 -08:00
David Aguilar 5dc0868487 controllers.classic: Micro-optimize expanding folders
Avoid looking up the 'entry' function each time by looking it up once
outside of the loop body.

Signed-off-by: David Aguilar <davvid@gmail.com>
2010-12-28 02:57:31 -08:00
David Aguilar 5905459466 models.commit: Remove call to popen.kill()
That function doesn't even exist.  This is in error-handling that
only happens when you forcibly kill cola while loading the dag view.

Signed-off-by: David Aguilar <davvid@gmail.com>
2010-12-28 02:30:11 -08:00
David Aguilar ee38c44863 cola.settings: Guard against malformed json
If for whatever reason ~/.cola contains bad json cola won't startup.
Act as if the file didn't exist instead.

Signed-off-by: David Aguilar <davvid@gmail.com>
2010-12-28 02:00:01 -08:00
David Aguilar cd5c1bb039 darwin: Add git-osx-installer $PATH
Add common /usr/local/bin and /usr/local/git/bin to $PATH
in the OS X wrapper script.

Signed-off-by: David Aguilar <davvid@gmail.com>
2010-12-27 19:34:31 -08:00
David Aguilar cdc2011134 Makefile: Generate git-cola-$(version).app.tar.gz
This makes it consistent with the Windows installer.

Signed-off-by: David Aguilar <davvid@gmail.com>
2010-12-27 19:13:40 -08:00
David Aguilar 5e19e3ecb1 doc: Add 1.4.3 release notes
Signed-off-by: David Aguilar <davvid@gmail.com>
2010-12-27 18:56:47 -08:00
David Aguilar ec271b19bb darwin: Remove dependency on py2app for OS X deployment
We now ship separate installers for Qt/PyQt so we don't need to
ship them inside of git-cola.app anymore.  This cuts down on the
OS X download size tremendously and allows us to ship a single
git-cola.app for all OS X platforms.

The only difference between platforms are the Qt/PyQt libraries
which are available as a separate download.

Signed-off-by: David Aguilar <davvid@gmail.com>
2010-12-27 15:07:38 -08:00
David Aguilar 9683fa918b darwin: Raise cola only after showing the main widget
Qt on OS X doesn't honor raise_() call unless show() has been
called so swap the order.

Signed-off-by: David Aguilar <davvid@gmail.com>
2010-12-27 07:50:13 -08:00
David Aguilar 50edfcf994 gitcmds,qtutils: Guard against unicode file paths
Paths with unicode are not supposed to hit the "else:" block in eval_path()
but guard against it anyways by decoding inputs and encoding outputs.

Closes #64

Signed-off-by: David Aguilar <davvid@gmail.com>
2010-12-06 23:16:19 -08:00
David Aguilar 61ae73fc2e gitcmds: "globals to locals" micro-optimization
Signed-off-by: David Aguilar <davvid@gmail.com>
2010-12-06 23:00:33 -08:00
David Aguilar e67e142e8f gitcmds: Honor gui.diffcontext by using the right configuration variable
cola mistakenly used "diff.context" when it meant "gui.diffcontext"
per git-config(1).

Signed-off-by: David Aguilar <davvid@gmail.com>
2010-12-06 22:59:12 -08:00
David Aguilar 121f0aee66 gitcmds: Centralize diff options to guard against diff.external
Refactor usage of 'git diff' to pass '--no-ext-diff'.
Avoid launching user-configured diff.external viewers by updating
the _common_diff_opts() function and ensuring that all git.diff
callers use it.

Closes #67

Signed-off-by: David Aguilar <davvid@gmail.com>
2010-12-06 22:56:21 -08:00
David Aguilar 29a5e71745 dag: Automatically size widget relative to the desktop size
Signed-off-by: David Aguilar <davvid@gmail.com>
2010-11-01 00:17:33 -07:00
David Aguilar 09e851a987 test: Fix test_currentbranch()
Clear the stat cache during test setup so that there
is no test cross-pollination.

Signed-off-by: David Aguilar <davvid@gmail.com>
2010-10-24 23:29:12 -07:00
David Aguilar 3a064b687f test/tmp: Add .gitignore so that this directory always exists
Signed-off-by: David Aguilar <davvid@gmail.com>
2010-10-24 23:19:35 -07:00
David Aguilar 85a4908e96 test/helper.py: Remove debug print message
Signed-off-by: David Aguilar <davvid@gmail.com>
2010-10-24 23:17:04 -07:00
David Aguilar 3f88b33d9b gitcfg: Fix gitcmds test by clearing the cache key during reset()
Ensure that we re-read the configs after reset() by clearing the
stat cache.

Signed-off-by: David Aguilar <davvid@gmail.com>
2010-10-24 23:15:14 -07:00
David Aguilar edbab74c55 test: Remove unnecessary module functions
Signed-off-by: David Aguilar <davvid@gmail.com>
2010-10-24 23:14:14 -07:00
David Aguilar 8906ada850 test: Fix the gitcfg test case
Avoid globals in the helper testlib so that every test
gets its own temp repository created by tempfile.mkdtemp().

Signed-off-by: David Aguilar <davvid@gmail.com>
2010-10-24 22:52:12 -07:00
David Aguilar c1a6023566 dag: Add a useful widget to show the currently selected commit
When a node is clicked we copy display commit information and
copy its SHA-1 into the copy/paste buffer.  This can be expanded
to have more functionality such as cherry-pick, format-patch, branch,
merge, rebase, etc.

Signed-off-by: David Aguilar <davvid@gmail.com>
2010-10-23 18:56:17 -07:00
David Aguilar c06f7dc755 gitcmds: Add 'diff_info()' to provide text with a custom header and diff
Signed-off-by: David Aguilar <davvid@gmail.com>
2010-10-23 18:56:17 -07:00
David Aguilar 6b6c0c310c views.mainwindow: Remove unused import
Signed-off-by: David Aguilar <davvid@gmail.com>
2010-10-23 18:56:17 -07:00
David Aguilar 6f80a9f4a8 dag: Set default zoom to 2x
Signed-off-by: David Aguilar <davvid@gmail.com>
2010-10-23 18:56:17 -07:00
David Aguilar c4becc0683 dag: Limit SHA-1 display to 8 characters
Signed-off-by: David Aguilar <davvid@gmail.com>
2010-10-23 18:56:17 -07:00
David Aguilar 60c679a23f views.dag: Add a control area to the dag widget
Signed-off-by: David Aguilar <davvid@gmail.com>
2010-10-23 18:56:17 -07:00
David Aguilar 6bf3db4449 Revert "setup.py: do not replace shebang lines in python scripts"
This reverts commit 544032b2d6.

Arch's default 'python' is python3k so prepare for this by
re-enabling distutil's shebang line replacement.

This allows packagers to supply the $(PYTHON) variable
to 'make' and have it do the right thing.  e.g.:

	make PYTHON=python2 prefix=/usr install

This causes the installed 'git-cola' script to have have
the correct shebang line.

Closes #66

Reported-by: alyst via github.com
Signed-off-by: David Aguilar <davvid@gmail.com>
2010-10-22 15:53:13 -07:00
David Aguilar 0383e34bdf doc: Final 1.4.2.5 release notes
Signed-off-by: David Aguilar <davvid@gmail.com>
2010-10-20 01:30:20 -07:00
David Aguilar 8a022389c1 doc: Add initial 1.4.2.5 release notes
Signed-off-by: David Aguilar <davvid@gmail.com>
2010-10-20 01:22:30 -07:00
David Aguilar 5173faed40 Makefile: Remove hard-coded lib/ path
Ask python for the default LIBDIR and use that to calculate the
path to the leftover eggs produced by distutils.

Signed-off-by: David Aguilar <davvid@gmail.com>
2010-10-20 01:21:20 -07:00
David Aguilar 10e7da10c9 views.status: Call the wrapped mouse release callback in the wrapper
We wrap the status view's QTreeWidget mouse release event to clear the
selection when clicking in an empty area.  We were not calling the
wrapped objecct's original event handler which caused odd behavior
in newer versions of PyQt.  Call into the original handler to get
the correct behavior.

Signed-off-by: David Aguilar <davvid@gmail.com>
2010-10-20 01:03:40 -07:00
David Aguilar dd6bb1acb2 qt: Check layouts against None instead of assuming bool(layout) is True
PyQt 4.7.3 no longer evaluates bool(layout) to True.
Thanks to the patient Ubuntu and Arch users who reported this problem.

Closes #62

Signed-off-by: David Aguilar <davvid@gmail.com>
2010-10-20 00:25:11 -07:00
David Aguilar cb382e4357 views.status: Copy selected path to clipboard when clicked
Signed-off-by: David Aguilar <davvid@gmail.com>
2010-08-29 19:28:09 -07:00
David Aguilar fb294f3a9b views.main: Make Ctrl+C "copy to clipboard" in the diff viewer
Closes #63

Signed-off-by: David Aguilar <davvid@gmail.com>
2010-08-25 01:09:08 -07:00
David Aguilar b14ff6c261 doc: Add 1.4.2.4 release notes
Signed-off-by: David Aguilar <davvid@gmail.com>
2010-08-24 17:50:13 -07:00
David Aguilar 2bbe2c2537 doc: Update references to 'cola.gitcmd'
'cola.gitcmd' got merged into 'cola.git'.

Signed-off-by: David Aguilar <davvid@gmail.com>
2010-08-17 15:45:08 -07:00
David Aguilar dada04c548 status: Remove "single click to (un)stage"
Uri recommended we get rid of this.
It's confusing and disorienting to have different behavior
based on where you click on an item.

Signed-off-by: David Aguilar <davvid@gmail.com>
Signed-off-by: Uri Okrent <uokrent@gmail.com>
2010-08-17 15:42:01 -07:00
David Aguilar 7154f5e16c actions: Add ability to create annotated tags
cola now creates annotated tags when a tag message is
supplied.  Previously, cola allowed creating tag messages
creating signed tags only.

Signed-off-by: David Aguilar <davvid@gmail.com>
2010-08-17 10:32:02 -07:00
David Aguilar 35ea2cefdf QFlowLayoutWidget: Hold a reference to the layout
This ensures that Python won't try to garbage-collect the layout.

Signed-off-by: David Aguilar <davvid@gmail.com>
2010-08-02 12:14:13 -07:00
David Aguilar a5756bf09b Revert "mainwindow: Simplify the 'Actions' widget"
This reverts commit 73acd20d23.

I was reading bug #62 incorrectly.
They were talking about the actions _dialog_,
not the actions widget ;-)

Conflicts:

	cola/views/mainwindow.py

Signed-off-by: David Aguilar <davvid@gmail.com>
2010-08-02 01:01:14 -07:00
David Aguilar 760a6a72cd cmds: Remove reference to non-existent utils.shell_quote()
utils.shell_quote() was removed in favor of commands.mkarg().

Closes #62

Signed-off-by: David Aguilar <davvid@gmail.com>
2010-08-02 00:58:25 -07:00
David Aguilar c1dc4181bf mainwindow: Hold a reference to the actions widget's layout
This shouldn't be needed but it may work around a bug in
PyQt4 4.7.4.

Signed-off-by: David Aguilar <davvid@gmail.com>
2010-08-02 00:54:04 -07:00
David Aguilar 0276f1b000 doc: Add 1.4.2.3 release notes
Signed-off-by: David Aguilar <davvid@gmail.com>
2010-07-31 07:29:07 -07:00
David Aguilar 73acd20d23 mainwindow: Simplify the 'Actions' widget
PyQt4 4.7.4 loses its bearings when a widget modifies the
direction of its QBoxLayout in a resize event.

We depended on this behavior to allow the actions widget to
automatically switch between vertical and horizontal layouts.

Simplify the code by just sticking to a vertical layout.
All of the actions on the 'Actions' widget are also
available in the menus these days.

Closes #62

Reported-by: alyst via github.com
Signed-off-by: David Aguilar <davvid@gmail.com>
2010-07-31 05:27:47 -07:00
David Aguilar 901a8a2a57 usability: Allow un/staging all by right-clicking on top-level items
We can now stage or unstage all files by right-clicking on the
'Staged' or 'Unstaged' icons.

Closes #57

Suggested-by: Shish Moom <webmaster@shishnet.org> via github
Signed-off-by: David Aguilar <davvid@gmail.com>
2010-07-27 04:24:32 -07:00
David Aguilar 995877a653 unicode: Encode paths passed to slurp() and write()
Closes #61

Signed-off-by: David Aguilar <davvid@gmail.com>
2010-07-27 03:27:52 -07:00
David Aguilar 2bdae67221 usability: Allow staging all files when committing
Staging files one at a time is a pain; normally from the
command line I just do 'commit -a'.

A GUI equivalent would be nice. Instead of 'Commit' coming
back with 'There are no staged files die', it could say,

'There are no staged files.
 Do you want to stage and commit all modified files?'

Closes #55

Signed-off-by: David Aguilar <davvid@gmail.com>
2010-07-24 12:42:11 -07:00
David Aguilar 802e0811dd mainwindow: Add 'Fetch', 'Push', and 'Pull' to the actions menu
Having these available in the 'Actions' menu would be useful
and more intuitive.  This also allows us to save some screen
real-estate since we no longer need the 'Actions' widget to
access these commands.

Closes #58

Signed-off-by: David Aguilar <davvid@gmail.com>
2010-07-24 12:19:01 -07:00
David Aguilar 1243ab61a2 i18n: Unconditionally install a gettext translator
We were only calling i18n.install() when 'locale' was passed to
the ColaApplication constructor.  We should call it unconditionally
otherwise we won't be able to translate anything.

Additionally, call i18n.gettext() in create_button() so that
the action buttons are translated too.

Signed-off-by: David Aguilar <davvid@gmail.com>
2010-07-24 11:56:53 -07:00
David Aguilar 1cc942976f mainwindow: Make action button labels consistent
'Push...' should have three trailing dots.

Signed-off-by: David Aguilar <davvid@gmail.com>
2010-07-24 11:52:39 -07:00
David Aguilar e5b35c1af5 darwin: Add command classes from 'extra' when building git-cola.app
Signed-off-by: David Aguilar <davvid@gmail.com>
2010-07-22 14:42:00 -07:00
David Aguilar aadd26c00a Update release notes for 1.4.2.2
Signed-off-by: David Aguilar <davvid@gmail.com>
2010-07-22 14:11:54 -07:00
David Aguilar c4355b28e2 gitcfg: Use os.path.join() to construct the path to ~/.gitconfig
Signed-off-by: David Aguilar <davvid@gmail.com>
2010-07-08 01:08:16 -07:00
David Aguilar 7e970792a4 ssh-askpass: Do not hang on exit
ssh-askpass's window sometimes freezes after the
password is typed.  Each exit causes the window
to be destroyed again, resulting in a hang.

The destroy event might come from the window manager
so rebind <Destroy> inside of its handler before exiting.

Signed-off-by: David Aguilar <davvid@gmail.com>
2010-07-07 01:08:34 -07:00
David Aguilar 34762da4fa git: Replace utils.shell_quote() usage with commands.mkarg()
Signed-off-by: David Aguilar <davvid@gmail.com>
2010-06-17 14:07:10 -07:00
David Aguilar 27b0eadcde portability: Move cola.commands to cola.cmds
Otherwise we can't "import commands" from the Python stdlib.

Signed-off-by: David Aguilar <davvid@gmail.com>
2010-06-17 14:07:10 -07:00
David Aguilar effbfb6270 git: Fold cola.gitcmd into cola.git
These two are very closely related so they might as well
live in the same module.

Signed-off-by: David Aguilar <davvid@gmail.com>
2010-06-17 14:07:10 -07:00
David Aguilar 171594a61a doc: Add 1.4.2.2 release notes
Signed-off-by: David Aguilar <davvid@gmail.com>
2010-06-17 14:06:28 -07:00
David Aguilar b53e69823f views.dag: Add a visual indicator for merge commits
They're slightly darker.

Signed-off-by: David Aguilar <davvid@gmail.com>
2010-06-17 14:05:49 -07:00
David Aguilar 87d0848d38 views.dag: Improve the graph layout algorithm
This algorithm is slightly more appealing because it bends chains
of commits that lead to parallel branches.

It's not perfect, but it's one of the simpler things we can do
with the streaming one-node-at-a-time approach in use.

We could probably make a very nice layout by post-processing
the graph using a thread but it's not *that* bad, either.

Signed-off-by: David Aguilar <davvid@gmail.com>
2010-06-17 14:05:21 -07:00
David Aguilar 203d59b115 views.dag: Use the parent/child attributes from Commit objects
We don't need to track this in the view now that the commit objects
provide them.

Signed-off-by: David Aguilar <davvid@gmail.com>
2010-06-17 14:02:53 -07:00
David Aguilar 19bdbed35a views.dag: Make mouseWheel panning faster
Increase the scale factor so that panning with the mouse wheel
translates more.

Signed-off-by: David Aguilar <davvid@gmail.com>
2010-06-17 14:01:24 -07:00
David Aguilar da991ab186 views.dag: Decrease the minimum zoom factor
Signed-off-by: David Aguilar <davvid@gmail.com>
2010-06-17 14:00:25 -07:00
David Aguilar a6a73211cc views.dag: Remove unneeded itemChange() callback
Signed-off-by: David Aguilar <davvid@gmail.com>
2010-06-17 13:59:33 -07:00
David Aguilar 6beea81452 views.dag: Remove calls to Edge.adjust() in Node.add_edge()
There's no reason to do this in add_edge() since the information
is incomplete on the first call.  We now handle calling adjust()
outside of add_edge(), so this call is unnecessary.

Signed-off-by: David Aguilar <davvid@gmail.com>
2010-06-17 13:59:07 -07:00
David Aguilar 186b1f15f2 views.dag: Do not set the Z value in Node.paint()
There's no reason to do this if we're not going to allow moving nodes.

Signed-off-by: David Aguilar <davvid@gmail.com>
2010-06-17 13:57:43 -07:00
David Aguilar 3d57a83450 views.dag: Micro-optimize Node type(), shape(), boundingRect(), and glyph()
Signed-off-by: David Aguilar <davvid@gmail.com>
2010-06-17 13:57:07 -07:00
David Aguilar ea8ab2dc11 views.dag: Micro-optimize Edge.type()
Signed-off-by: David Aguilar <davvid@gmail.com>
2010-06-17 13:55:59 -07:00
David Aguilar ae51a72acb views.dag: Remove 'graph' from the Node constructor
This was unused.

Signed-off-by: David Aguilar <davvid@gmail.com>
2010-06-17 13:55:32 -07:00
David Aguilar c46c1a81b9 models.commit: CommitFactory for creating Commit objects
This allows us to lazy-load parent commits.
The advantage is that we now have parent commit objects that
are lazy-loaded with thier metadata.

Signed-off-by: David Aguilar <davvid@gmail.com>
2010-06-17 13:53:47 -07:00
David Aguilar 980d874889 models.commit: Remove unused 'commits()' function
Signed-off-by: David Aguilar <davvid@gmail.com>
2010-06-17 11:37:23 -07:00
Michael Geddes 96f4423b31 Fix staging lines/hunks in win32.
Patches are required to be without CRLF endings
so open/write files in binary mode.

Closes #53

Signed-off-by: Michael Geddes <michael@frog.wheelycreek.net>
Signed-off-by: David Aguilar <davvid@gmail.com>
2010-06-15 21:24:57 -07:00
David Aguilar 15b3d95ca6 qtutils: Do not memoize create_treewidget_item()
It causes problems in the status tree widget.

Signed-off-by: David Aguilar <davvid@gmail.com>
2010-06-14 20:28:19 -07:00
David Aguilar 5d00b99974 decorators: Replace 'FunctionMaker' with a simple function
We don't need any of the function signature-mangling features
so let's just rewrite this code.  This is also faster since it
no longer needs the 're' and 'inspect' modules.

Signed-off-by: David Aguilar <davvid@gmail.com>
2010-06-14 15:41:33 -07:00
David Aguilar 3dce4c9732 views.dag: memoize() expensive graph drawing methods
shape() and boundingRect() were heavy hitters when profiling the
'git dag' using cProfile.  Use memoize() to optimize away their
expensive calculations.

Signed-off-by: David Aguilar <davvid@gmail.com>
2010-06-08 14:11:51 -07:00
David Aguilar 6c47625e08 qtutils: memoize() icon creation and gettext translations
Eliminate more local caches in favor of the memoize() solution.
This makes the code easier to read and simpler in implementation.

Signed-off-by: David Aguilar <davvid@gmail.com>
2010-06-08 14:07:20 -07:00
David Aguilar 2ea82700aa qtutils: Ensure two newlines between each function definition
Signed-off-by: David Aguilar <davvid@gmail.com>
2010-06-08 14:03:45 -07:00
David Aguilar 0259c4ca0a utils: memoize() platform introspection functions
We probably don't gain too much by memoizing is_win32() but it
probably doesn't hurt to shave off an instruction or two either.

Signed-off-by: David Aguilar <davvid@gmail.com>
2010-06-08 14:01:20 -07:00
David Aguilar ac21b569d3 decorators: Use memoize() to implement all factory functions
This eliminates the global variables used to implement
singleton factories throughout cola.

Signed-off-by: David Aguilar <davvid@gmail.com>
2010-06-08 13:59:27 -07:00
David Aguilar 754acf8a4b version: Use memoize() to simplify and speedup version comparisons
This eliminates all of the module globals by using memoize().
The code is simpler too since it does not have to handle caching.

Signed-off-by: David Aguilar <davvid@gmail.com>
2010-06-08 13:55:12 -07:00
David Aguilar 271fd9a199 Add cola.decorators to provide the memoize() decorator
http://en.wikipedia.org/wiki/Memoization

We're including code from python-decorator for ease of installation.
Our addition is the memoize() decorator.  memoize() will allow us to
simplify many of our singleton factory methods.

Signed-off-by: David Aguilar <davvid@gmail.com>
2010-06-08 13:52:47 -07:00
David Aguilar 904b126eae mainwindow: Add '...' to fetch, push, pull, and stash buttons
These launch a separate dialog so use '...' to provide a visual indication.

Closes #51

Signed-off-by: David Aguilar <davvid@gmail.com>
2010-06-02 14:56:38 -07:00
Uri Okrent 89f0036448 Cola only needs pyqt4 >= 4.1
Signed-off-by: Uri Okrent <uokrent@disneyanimation.com>
2010-05-26 18:11:22 -07:00
David Aguilar b381361430 doc: Add 1.4.2.1 release notes
Signed-off-by: David Aguilar <davvid@gmail.com>
2010-05-19 00:34:44 -07:00
David Aguilar 1d028ba3cc commands: Don't expandvars() the command since 'sh -c' doess it for us
Signed-off-by: David Aguilar <davvid@gmail.com>
2010-05-08 02:11:03 -07:00
David Aguilar 864af552bb commands.RunConfigAction: Fix uninitialized 'title' variable
Signed-off-by: David Aguilar <davvid@gmail.com>
2010-05-05 22:00:26 -07:00
Michael Geddes e691073611 Use 'sh -c' for menu commands
git-gui uses 'sh -c $cmd' when implementing menu commands.
Passing shell=True works fine on Linux, but on Windows
it goes through cmd.exe, which is not the shell we want
to use.

Always use 'sh -c' so that behavior is consistent
across platforms.

Signed-off-by: Michael Geddes <michael@frog.wheelycreek.net>
Signed-off-by: David Aguilar <davvid@gmail.com>
2010-05-05 21:55:33 -07:00
David Aguilar aba3869285 perf: Optimize away a few more calls to 'git' when unstaging
The unstaging operation no longer calls any of the following:

	git update-index --refresh
	git diff-index --cached HEAD
	git diff-index HEAD

We now simply unstage the file using 'git reset' and then ask
git for the list of all files using 'git ls-files -z'.

We can then classify each unstaged path as being either
modified or untracked based solely on whether or not each
path is a member of the set of known files.

This should noticibly speed things up when working with large
repositories.

Closes #48

Signed-off-by: David Aguilar <davvid@gmail.com>
2010-05-05 15:32:57 -07:00
David Aguilar 378a08c165 perf: Optimize Unstage command to minimize git calls
This shaves a few more git calls from the unstage command
which makes things more pleasant everywhere, and on Windows
in particular.

Closes #48

Signed-off-by: David Aguilar <davvid@gmail.com>
2010-05-04 01:57:54 -07:00
David Aguilar bb108320c7 models.main: Optimize stage_paths() to avoid git calls
This fixes the performance issues when staging paths.
Fixing the performance of the Unstage command will require a
different strategy.  Dulwich and similar libraries are longer-term
things to consider.

Closes #48

Signed-off-by: David Aguilar <davvid@gmail.com>
2010-05-04 01:11:18 -07:00
David Aguilar b2ec47f2f0 views.dag: Read history in a background thread
Signed-off-by: David Aguilar <davvid@gmail.com>
2010-05-04 00:18:58 -07:00
David Aguilar 90271f4af7 views.dag: Tighten padding around the scene rect
Signed-off-by: David Aguilar <davvid@gmail.com>
2010-05-04 00:16:24 -07:00
David Aguilar 16676b6f1e views.dag: Increase mousewheel velocity
Signed-off-by: David Aguilar <davvid@gmail.com>
2010-05-04 00:15:42 -07:00
David Aguilar feb81bf0f4 commit: Add a history traversal iterator
RepoReader streams git rev-list for reading history fom git.

Signed-off-by: David Aguilar <davvid@gmail.com>
2010-05-04 00:09:05 -07:00
David Aguilar e95d37b3e1 commit: Use the full SHA-1 internally
Signed-off-by: David Aguilar <davvid@gmail.com>
2010-05-04 00:05:55 -07:00
David Aguilar 4460ab0c55 commit: Make str() return the SHA-1
Signed-off-by: David Aguilar <davvid@gmail.com>
2010-05-04 00:04:20 -07:00
David Aguilar 1591217955 doc: Document models.commit
Signed-off-by: David Aguilar <davvid@gmail.com>
2010-05-04 00:03:39 -07:00
David Aguilar 168c7aa7e9 views.main: Remove tooltip from the status view too
That widget is also informational so we don't want to block it with
the tooltip message.

Signed-off-by: David Aguilar <davvid@gmail.com>
2010-05-04 00:03:07 -07:00
David Aguilar 752367ee92 views.dag: Allow running views.dag from the source tree
% python cola/views/dag.py

Signed-off-by: David Aguilar <davvid@gmail.com>
2010-05-04 00:00:24 -07:00
David Aguilar 876b5d2694 views.dag: Remove unused import
Signed-off-by: David Aguilar <davvid@gmail.com>
2010-05-04 00:00:24 -07:00
Uri Okrent c91b6ae972 compat: Make a fake hashlib for use on python < 2.5
Signed-off-by: Uri Okrent <uokrent@gmail.com>
2010-04-28 19:06:45 -07:00
David Aguilar d889bc25e6 portability: Add support for PyQt 4.1
cola is now certified for Python 2.4 and PyQt 4.1.
Although this combination is fairly old, this allow
us to run on el5, which is still in use in many places.

Some features are disabled when using PyQt < 4.3
but most of the functionality works just fine.

Signed-off-by: David Aguilar <davvid@gmail.com>
2010-04-28 15:55:37 -07:00
Stefan Naewe a4bd13e365 win32: Remove /q from InnoSetup compiler to make it work
Without this patch the InnoSetup compiler errors out and doesn't
create the installer executable.

/q works fine with InnoSetup 5.1.14 but errors out starting
with 5.2.4-dev.  Modern versions (e.g. 5.3.8 and 5.3.9)
both error out without this patch.

Signed-off-by: Stefan Naewe <stefan.naewe@atlas-elektronik.com>
Signed-off-by: David Aguilar <davvid@gmail.com>
2010-04-22 22:41:27 -07:00
David Aguilar d2e8706845 README: List publicate dates first, shorten 3rdparty modules section
Signed-off-by: David Aguilar <davvid@gmail.com>
2010-04-21 02:06:38 -07:00
David Aguilar 2835cf60ab views.main: Limit tooltip to the status and commit widgets only
The tooltip gets in the way of the diff view so let's limit it
to just the status and commit message dock widgets.

Signed-off-by: David Aguilar <davvid@gmail.com>
2010-04-20 18:51:51 -07:00
David Aguilar 8bba79138d doc: Add 1.4.2 release notes
Signed-off-by: David Aguilar <davvid@gmail.com>
2010-04-20 12:21:31 -07:00
David Aguilar c744c25bc6 doc: Update documentation for the DAG view and fix sphinx warnings
Signed-off-by: David Aguilar <davvid@gmail.com>
2010-04-20 08:41:34 -07:00
David Aguilar 925d0ff765 doc: Use $(SED) where appropriate
Signed-off-by: David Aguilar <davvid@gmail.com>
2010-04-20 01:48:23 -07:00
David Aguilar 923e431098 doc: Update copyright
Signed-off-by: David Aguilar <davvid@gmail.com>
2010-04-20 01:47:53 -07:00
David Aguilar 7e47d660f6 doc: Use shell-friendly sentinel chars in conf.py.in
msysgit sed needs escaped {}s and linux sed barfs when they're escaped.
Avoid the portability issues altogether by using @@ instead.
Thanks to Stefan Naewe for pointing out the issue.

Signed-off-by: David Aguilar <davvid@gmail.com>
2010-04-20 01:47:43 -07:00
Stefan Naewe e73210682c doc: use \{,\} in sed expression to get version/release substitution correct
otherwise one gets e.g. "1.4.1.2 = '{version}'" in conf.py

Signed-off-by: Stefan Naewe <stefan.naewe@googlemail.com>
Signed-off-by: David Aguilar <davvid@gmail.com>
2010-04-19 18:13:01 -07:00
David Aguilar 794efbc3be doc: Use 'sed' instead of 'perl' in the Makefile
There's very few places where we require perl so let's
use the more portable option whenever possible.
We were using perl to do string replacements when
generating conf.py.  We can just as easily use 'sed'
and eliminate the pipe while we're at it.

Signed-off-by: David Aguilar <davvid@gmail.com>
2010-04-17 14:31:32 -07:00
David Aguilar 818edc00d7 doc: Update the contributors list to include Stefan Naewe
Signed-off-by: David Aguilar <davvid@gmail.com>
2010-04-17 14:31:32 -07:00
Stefan Naewe 6e5c7d65fa doc: don't let perl do inplace modifications of conf.py
Perl always gives the error "Can't do inplace edit on conf.py: Permission denied."
when trying to build any documentation.
This replaces inplace modifications by command piping.

Signed-off-by: Stefan Naewe <stefan.naewe@googlemail.com>
Signed-off-by: David Aguilar <davvid@gmail.com>
2010-04-17 14:17:31 -07:00
David Aguilar bd86c6f1e2 gitcmds: Fix merge_message() when FETCH_HEAD doesn't exist
Signed-off-by: David Aguilar <davvid@gmail.com>
2010-04-16 02:04:23 -07:00
David Aguilar b7525b6072 doc: Fold AUTHORS file into the intro page for git-cola
Signed-off-by: David Aguilar <davvid@gmail.com>
2010-04-14 00:21:06 -07:00
Uri Okrent f0f8ff7e20 cola.views.remote: qt 4.5, QLineEdit.setFocus takes no arguments
Signed-off-by: Uri Okrent <uokrent@gmail.com.com>
2010-04-13 13:27:22 -07:00
Uri Okrent 76e47a0d2f difftool: running on modified+staged path shouldn't show staged changes
Running difftool on a path that is both modified and staged can be done
from two spots in the repo status tool, the staged version of the path
and the modified version of that path.  The difftool run on the staged
version was correctly only showing the staged changes, but run on the
modified section was showing both the staged and modified changes, when
it should really leave out the staged changes.

Signed-off-by: Uri Okrent <uokrent@gmail.com>
Signed-off-by: David Aguilar <davvid@gmail.com>
2010-04-13 13:22:02 -07:00
David Aguilar dd17e89996 views.actions: Add a command wrapper for run_command and use it
This removes the GUI dependency from the RunConfigAction command object
introduced in a09a151147.

Signed-off-by: David Aguilar <davvid@gmail.com>
2010-04-13 01:16:53 -07:00
David Aguilar fd8706cd24 commands: Use utils.run_command() instead of subprocess.call()
It's best to avoid using call() when we can use subprocess with shell=True.

Signed-off-by: David Aguilar <davvid@gmail.com>
2010-04-13 01:16:53 -07:00
David Aguilar eaef138da2 commands: Expand title again after setting environment variables
Signed-off-by: David Aguilar <davvid@gmail.com>
2010-04-13 01:16:53 -07:00
David Aguilar 014e0e7b62 views.command: Remove reference to non-existent self.start variable
Signed-off-by: David Aguilar <davvid@gmail.com>
2010-04-13 01:16:53 -07:00
David Aguilar 1f08e64fc4 views.actions: Fix action dialog when revprompt is true and argprompt is false
You can't hide a layout; you have to hide its members.

Signed-off-by: David Aguilar <davvid@gmail.com>
2010-04-13 01:16:53 -07:00
David Aguilar 88ae28e22f views.command: Support Unicode strings in the command console
Signed-off-by: David Aguilar <davvid@gmail.com>
2010-04-13 01:16:53 -07:00
David Aguilar 7b8d379d45 commands, views.actions: Fix typos and import QtCore.SIGNAL()
The mistyped signal was preventing stderr from being reported.
Also, use SIGNAL() to shorten long lines.

Signed-off-by: David Aguilar <davvid@gmail.com>
2010-04-13 01:16:53 -07:00
David Aguilar 16c7f267d8 utils: Add run_command() from git.git
We should probably move all of the infrastructure over to using
run_command().  It's probably better to just use communicate().

Signed-off-by: David Aguilar <davvid@gmail.com>
2010-04-13 01:16:53 -07:00
David Aguilar 52b3a0c6e0 Add Michael Geddes to the authors file
Signed-off-by: David Aguilar <davvid@gmail.com>
2010-04-13 01:16:53 -07:00
Michael Geddes a09a151147 Implement command console for configured actions
This adds the command output console as described in git-config(1).

Signed-off-by: Michael Geddes <michaelg@rbionline.com>
Signed-off-by: David Aguilar <davvid@gmail.com>
2010-04-13 00:59:12 -07:00
David Aguilar e3d9abaded cola.main: Update the "Here be dragons" trace message
It's probably about time we start adding some easter eggs.

Signed-off-by: David Aguilar <davvid@gmail.com>
2010-04-08 01:46:54 -07:00
David Aguilar 49107e6dfd controllers.remote: DTRT when doing fetch, push, pull
This sets appropriate defaults for the remote dialogs so that
we can simply click 'fetch', 'push', or 'pull' and it'll do the
normal expected thing, such as:

	git fetch origin
	git push origin master
	git pull origin master

Closes #43

Signed-off-by: David Aguilar <davvid@gmail.com>
2010-04-08 01:24:31 -07:00
David Aguilar 8c0df5e3b8 controllers.remote: Filter remote branches when selecting remotes
This uses a fnmatch pattern to find remote names matching
remote/*.  This is a common enough convention that we should
do it by default.  If nothing matches we fallback to showing
everything.

Signed-off-by: David Aguilar <davvid@gmail.com>
2010-04-08 01:24:31 -07:00
David Aguilar 8234444bd7 views.remote: Give focus to the topmost widget
This adds some action-dependent behavior for giving the appropriate
widget focus.

Signed-off-by: David Aguilar <davvid@gmail.com>
2010-04-08 01:24:31 -07:00
David Aguilar 29ab67ee1b views.remote: Refactor layout to make sense for fetch
Widget layout is neatly tucked away in a separate functions.
This let us easily special-case fetch to move the remote to
the top of the GUI.

Signed-off-by: David Aguilar <davvid@gmail.com>
2010-04-08 01:24:31 -07:00
David Aguilar 8b0dfd84c4 controllers.remote: Observe a clone of the model
This is a transient dialog and shouldn't affect the app model's
state so we should hande it a clone.

Signed-off-by: David Aguilar <davvid@gmail.com>
2010-04-08 01:24:31 -07:00
David Aguilar 58dfe801d3 models.main: Add a serialilzer to run generate_remote_helpers()
We can't just clone() because we lazily generate the remote
helper functions (fetch, push, pull).  The serializer class
handles calling the helper after cloning.

Signed-off-by: David Aguilar <davvid@gmail.com>
2010-04-08 01:24:31 -07:00
David Aguilar ab164ab3c3 commands: Support guitool.*.{arg,rev}prompt booleans
Check for the value True so that we don't try to set
text fields using a boolean.

git-config(1) says that the *prompt variables can either
be a boolean or a string to display.  This teaches the
action dialog to correctly handle booleans.

Signed-off-by: David Aguilar <davvid@gmail.com>
2010-04-07 03:31:43 -07:00
David Aguilar d08f9d07de views.revselect: Don't provide a default revision
It's better to make users give us an explicit expression
instead of assuming that 'HEAD' is always a good default.

Signed-off-by: David Aguilar <davvid@gmail.com>
2010-04-07 03:29:10 -07:00
David Aguilar 31266c7ee2 commands: Use prompt_user() instead of signalling information
This reads closer to what is going on -- we're prompting the
user.  Other signals don't have an interactive component
and thus use the regular notification->command map.

Signed-off-by: David Aguilar <davvid@gmail.com>
2010-04-07 03:27:18 -07:00
David Aguilar 1fefb8ac54 gitcfg: Support 'yes' and 'no' as valid booleans
Careful reading of git-config(1) revealed a bug in our
implementation.

Signed-off-by: David Aguilar <davvid@gmail.com>
2010-04-07 03:25:22 -07:00
David Aguilar e9eb3aafa8 commands: Add a RunConfigAction command
This implements the git guitool.*.cmd interface as documented
in git-config(1).

Closes #44

Suggested-by: frogonwheels via github
References: http://www.kernel.org/pub/software/scm/git/docs/git-config.html
Signed-off-by: David Aguilar <davvid@gmail.com>
2010-04-06 21:30:41 -07:00
David Aguilar dfe9ddc264 commands: Add a todo note to use mimetypes 2010-04-06 21:19:37 -07:00
David Aguilar dea0d057cd views: Add a command wrapper for the action runner dialog
This will allow us to run the dialog from the command object
through dependency injection.  GUIs register themselves with
the factory through which the commands ask for user input.
This keeps things decoupled yet flexible.
The following commits will integrate this into the main GUI.

Signed-off-by: David Aguilar <davvid@gmail.com>
2010-04-06 21:18:54 -07:00
David Aguilar 5d08751089 guicmds: Add a command wrapper for question() and information()
Signed-off-by: David Aguilar <davvid@gmail.com>
2010-04-06 21:13:46 -07:00
David Aguilar 71da727f0f guicmds: Fix docstring :param: syntax.
Signed-off-by: David Aguilar <davvid@gmail.com>
2010-04-06 21:13:31 -07:00
David Aguilar 4fd4187ede doc/views: Remove unused misc.rst
Signed-off-by: David Aguilar <davvid@gmail.com>
2010-04-06 21:12:38 -07:00
David Aguilar 24f74bf25c doc/views: Document dag and add dag + revselect to the index
Signed-off-by: David Aguilar <davvid@gmail.com>
2010-04-06 21:11:10 -07:00
David Aguilar 719d6dbb59 doc: Document the cola.qt module
Signed-off-by: David Aguilar <davvid@gmail.com>
2010-04-06 21:09:26 -07:00
David Aguilar ce671cc42d doc: Remove controllers.util
Signed-off-by: David Aguilar <davvid@gmail.com>
2010-04-06 21:08:01 -07:00
David Aguilar 0ce2adafb0 views: Add revselect for selecting revisions
This dialog is actually a generic "radiobutton +
listwidget driven textbox" but we use it for selecting git refs.

Signed-off-by: David Aguilar <davvid@gmail.com>
2010-04-06 21:07:07 -07:00
David Aguilar 90e2540f50 views: Add revselect for selecting revisions
This dialog is actually a generic "radiobutton +
listwidget driven textbox" but we use it for selecting git refs.

Signed-off-by: David Aguilar <davvid@gmail.com>
2010-04-06 21:06:55 -07:00
David Aguilar 63f20c460d fixup "classic: Reuse code during startup" 2010-04-06 21:04:58 -07:00
David Aguilar 98225274d2 gitcfg: Add helpers for guitool queries
These make it possible to ask for all options for a particular
guitool.

Signed-off-by: David Aguilar <davvid@gmail.com>
2010-04-06 21:02:53 -07:00
David Aguilar 7a644902fd gitcfg: Implement all() and find() for config queries
find() lets us search for config names with fnmatch globs.
all() makes it all possible by providing a flattened config
namespace.

Signed-off-by: David Aguilar <davvid@gmail.com>
2010-04-06 20:58:59 -07:00
David Aguilar 1a989e31b8 classic: Reuse code during startup
This simplifies main.py and that's good.

Signed-off-by: David Aguilar <davvid@gmail.com>
2010-04-06 20:56:40 -07:00
David Aguilar 594c4838c6 views.repo: Allow selecting directories
We just flag 'em as 'staged' when setting the selection.

Signed-off-by: David Aguilar <davvid@gmail.com>
2010-04-06 20:53:10 -07:00
David Aguilar 25044f6138 views.main: Unparent the DAG and classic apps
This lets us put these in the background, etc.

Signed-off-by: David Aguilar <davvid@gmail.com>
2010-04-06 20:52:03 -07:00
David Aguilar 562e3b06a8 signals: Add 'question'
We can do this now so let's start to use the ui factory
when writing commands.

Signed-off-by: David Aguilar <davvid@gmail.com>
2010-04-06 20:50:26 -07:00
David Aguilar 41e2f581e6 dag: Use center_on_screen() when standalone
Signed-off-by: David Aguilar <davvid@gmail.com>
2010-04-06 20:47:24 -07:00
David Aguilar d4408343aa gitcmds: Fix docstring syntax for :rtype:
Signed-off-by: David Aguilar <davvid@gmail.com>
2010-04-06 20:46:55 -07:00
David Aguilar c3db49ca10 qtutils: Add center_on_screen()
Helper function to place widgets in a simple location.

Signed-off-by: David Aguilar <davvid@gmail.com>
2010-04-06 20:45:03 -07:00
David Aguilar 90596ebd62 gitcmd: Update docstring to reflect usage
Signed-off-by: David Aguilar <davvid@gmail.com>
2010-04-06 20:37:38 -07:00
David Aguilar 60cf6d0c4b views.standard: Raise windows before showing them
Prevent dialogs from popping by ensuring they're in the foreground
before showing them.  Ya, we probably shouldn't hack show()
to mean show() + raise_(), but it's worked so far...

Signed-off-by: David Aguilar <davvid@gmail.com>
2010-04-06 20:34:37 -07:00
David Aguilar 6ef1c4d4d7 models.selection: Add 'filename' for returning a single path
This returns the first item that it finds in its various
staged, unstaged, etc. lists.

Signed-off-by: David Aguilar <davvid@gmail.com>
2010-04-06 20:12:25 -07:00
David Aguilar a98a95542e cmdfactory: Add 'prompt_user' for delegating user interaction
The command factory now allows us to prompt for user inputs.
This is done through a "Command Wrapper" class that is installed
for each parameter that needs user interaction from a command.

This allows the commands to be decoupled from their GUI
components.  The dependencies are injected at runtime by
the GUI.  Once the wrappers are in place then commands
can prompt for input by going through the command factory.

Signed-off-by: David Aguilar <davvid@gmail.com>
2010-04-06 20:11:34 -07:00
David Aguilar 8803186970 notification: Rename 'listen' to 'connect' for consistency
This makes the notification module more closely match the Qt
signals/slots API.

Signed-off-by: David Aguilar <davvid@gmail.com>
2010-04-06 15:45:23 -07:00
David Aguilar 84e74ae26f views.main: Factor out callbacks into guicmds
This removes another few hundreds lines of callback code
from the main GUI.  We also load less at startup.
controllers/util.py was folded into guicmds as well.

Signed-off-by: David Aguilar <davvid@gmail.com>
2010-04-04 01:01:24 -07:00
David Aguilar 83d8a45bee git: Log commands when GIT_COLA_TRACE is defined
Emit logging messages when the GIT_COLA_TRACE environment
variable is defined.  This lets users see the various git
commands run by cola during its execution.

	$ env GIT_COLA_TRACE=1 git cola

Closes #39

Signed-off-by: David Aguilar <davvid@gmail.com>
2010-04-03 23:28:00 -07:00
David Aguilar e26ceb332a views.dag: Draw decorations with a yellow background
Branches and tags are all drawn inside of the same rectangle.
We may want to parse local and remote branches out separately
later so that we can draw separate items for them.

Signed-off-by: David Aguilar <davvid@gmail.com>
2010-04-03 22:50:31 -07:00
David Aguilar 2b03b346e3 views.dag: Draw edges and arrows using a dotted gray line
Signed-off-by: David Aguilar <davvid@gmail.com>
2010-04-03 22:47:19 -07:00
David Aguilar 8418a06ff1 views.dag: Remove commented-out code
Signed-off-by: David Aguilar <davvid@gmail.com>
2010-04-03 22:46:47 -07:00
David Aguilar 604eb05352 views.dag: Factor out constants for drawing edges
This is a micro-optimization and may not make much of a difference
but it doesn't hurt, either.

Signed-off-by: David Aguilar <davvid@gmail.com>
2010-04-03 22:46:21 -07:00
David Aguilar 0f69c32b3e views.dag: Disable moving the graph nodes
We may later enable this if needed, but for now it's
better to have a static view.

Signed-off-by: David Aguilar <davvid@gmail.com>
2010-04-03 22:46:21 -07:00
David Aguilar 7a72bb3377 views.dag: Pass the parent widget to the dag view
This allows us to use the StandardDialog class as the base class
for the GitDAGWidget.  A benefit is that the DAG widget is
centered relative to the git-cola window.

Signed-off-by: David Aguilar <davvid@gmail.com>
2010-04-03 22:46:15 -07:00
David Aguilar e61d64f258 views.dag: Use the cola.compat module for the 'set' type
Signed-off-by: David Aguilar <davvid@gmail.com>
2010-04-03 22:39:08 -07:00
David Aguilar 575b268ac8 classic: Set the window title in response to model notifications
The RepoDialog observes the model so that we can detect when
we change branches, etc.

Signed-off-by: David Aguilar <davvid@gmail.com>
2010-04-03 22:39:08 -07:00
David Aguilar fa0c67d2f0 classic: Provide a dialog to wrap the RepoTreeView
Since we are now passing the Qt widget parent to the dialog
constructor we can do nice things like center the widget
relative to the git-cola window.

Signed-off-by: David Aguilar <davvid@gmail.com>
2010-04-03 22:39:08 -07:00
David Aguilar f25844ecaa models.commit: Filter out remote HEADs
Seeing origin/HEAD is probably a little confusing for new users
so filter it out of the decoration list.

Signed-off-by: David Aguilar <davvid@gmail.com>
2010-04-03 22:38:50 -07:00
David Aguilar a514ae4095 qtutils: Fix mistyped 'add_close_action()'
Originally mistyped as 'add_close_acction()'.

Signed-off-by: David Aguilar <davvid@gmail.com>
2010-04-03 22:02:33 -07:00
David Aguilar 246097d986 resources: Fallback to git-cola.txt for html_docs()
Launching 'git-cola' from the source tree won't find the html
docs by default so fallback to the asciidoc documentation.

Signed-off-by: David Aguilar <davvid@gmail.com>
2010-04-03 19:46:29 -07:00
David Aguilar a8fe8f498e doc: Document the --version and --git-path flags
Signed-off-by: David Aguilar <davvid@gmail.com>
2010-04-03 19:46:29 -07:00
David Aguilar 8a52a8acff README: Remove group email address
Implements spam paranoia.

Signed-off-by: David Aguilar <davvid@gmail.com>
2010-04-03 18:50:51 -07:00
David Aguilar 1d22e4edc8 README: Shorten document and update copyright for 2010
Less is more.

Signed-off-by: David Aguilar <davvid@gmail.com>
2010-04-03 18:47:37 -07:00
David Aguilar 3d8c79b070 views.main: Add a tooltip to show the current directory
Show a tooltip with the current directory and branch
for the main cola interface.

Closes #45

Suggested-by: Andrew Kinney <andrew.kinney@disneyanimation.com>
Signed-off-by: David Aguilar <davvid@gmail.com>
2010-04-03 17:46:52 -07:00
David Aguilar 5218878567 Merge branch 'treeview'
* treeview:
  dag: Move TODO file to the 'todo' branch
  views.dag: Show more edges
  git-dag: Show more nodes by default and limit to local branches
  views.dag: Add arrows to graph edges
  views.dag: Add GitDAGWidget
  views.dag: Add horizontal wheel panning
  views.dag: Update _zoom when scaling
  views.dag: Mouse wheel pans, add control to zoom
  views.dag: Zoom more
  Added TODO list for treeview branch
  views.main: Add 'DAG...' to the tools menu
  models.main: Factor out history methods into models.commit
  git: Remove the test for None
  Remove 'remotes/' and 'heads/' from branch names in dag view
  Add sha1 and branch/tag text to the nodes (and give them some space)
  Make room for text in the node by providing each with an internal glyph
  Set background color the simple way with the background brush
  Add additional interesting info to Commits
  treeview: add a graphical history viewer
  models: add a commits() method for traversing history
2010-04-01 16:46:55 -07:00
David Aguilar faa3eb34cf dag: Move TODO file to the 'todo' branch
The 'todo' branch has all of our meta information.

Signed-off-by: David Aguilar <davvid@gmail.com>
2010-04-01 16:45:55 -07:00
David Aguilar 3393af2f19 git-cola: Rework sys.path use system versions of thirdparty modules
This changes the launcher to prefer the user-installed versions
of simplejson and jsonpickle.

We want cola to work with whatever's installed on the system so
doing it this way ensures that we run against the system installed
modules whenever possible.

Signed-off-by: David Aguilar <davvid@gmail.com>
2010-04-01 16:43:26 -07:00
David Aguilar b8335b0dbb git-cola: Update copyright and license text for 2010
Remove the blurb about having received a copy of the GPL
since it is redundant.

Signed-off-by: David Aguilar <davvid@gmail.com>
2010-03-31 00:59:37 -07:00
David Aguilar b2dac151b7 resources: Document the install-docs target in html_docs()
Mention that index.html does not exist until after the install-docs
target has been run.  Update the docstrings for consistency while
we're at it.

Signed-off-by: David Aguilar <davvid@gmail.com>
2010-03-31 00:48:46 -07:00
David Aguilar 6c60f630dc i18n: Remove all references to the old qm infrastructure
Signed-off-by: David Aguilar <davvid@gmail.com>
2010-03-31 00:44:52 -07:00
David Aguilar 0cfa2788a0 views.dag: Show more edges
We limit edges longer than 2 ** 11.  Let's try 2 ** 13 instead.

Signed-off-by: David Aguilar <davvid@gmail.com>
2010-03-29 17:25:22 -07:00
David Aguilar ea135d02b4 git-dag: Show more nodes by default and limit to local branches
Signed-off-by: David Aguilar <davvid@gmail.com>
2010-03-29 17:25:22 -07:00
David Aguilar 31fab34793 views.dag: Add arrows to graph edges
Signed-off-by: David Aguilar <davvid@gmail.com>
2010-03-29 17:25:22 -07:00
David Aguilar 2e4793bccb views.dag: Add GitDAGWidget
The widget supports shortcut keys such as Ctrl+w.
We might want to split up the GUI to have a details section
which can react to the selection.

Signed-off-by: David Aguilar <davvid@gmail.com>
2010-03-29 17:25:22 -07:00
David Aguilar e34b28902f views.dag: Add horizontal wheel panning
Signed-off-by: David Aguilar <davvid@gmail.com>
2010-03-29 17:25:22 -07:00
David Aguilar 415f4c87a9 views.dag: Update _zoom when scaling
Signed-off-by: David Aguilar <davvid@gmail.com>
2010-03-29 17:25:22 -07:00
David Aguilar 0c61bd36ec views.dag: Mouse wheel pans, add control to zoom
We should add horizontal pan too.

Signed-off-by: David Aguilar <davvid@gmail.com>
2010-03-29 17:25:22 -07:00
David Aguilar f1a8070e23 views.dag: Zoom more
1.2 is a tad too slow.  This could be a config.. or not.

Signed-off-by: David Aguilar <davvid@gmail.com>
2010-03-29 17:25:22 -07:00
Uri Okrent 9bd2cb059b Added TODO list for treeview branch 2010-03-29 17:25:22 -07:00
David Aguilar 26074b70ea views.main: Add 'DAG...' to the tools menu
Signed-off-by: David Aguilar <davvid@gmail.com>
2010-03-29 17:25:22 -07:00
David Aguilar 863005d1ba models.main: Factor out history methods into models.commit
Signed-off-by: David Aguilar <davvid@gmail.com>
2010-03-29 17:25:15 -07:00
David Aguilar 4085f6c9ce git: Remove the test for None
None is undefined and we're not using it.

Signed-off-by: David Aguilar <davvid@gmail.com>
2010-03-29 17:07:18 -07:00
Uri Okrent 79c5c9a32f Remove 'remotes/' and 'heads/' from branch names in dag view
Signed-off-by: Uri Okrent <uokrent@gmail.com>
2010-03-29 17:07:18 -07:00
Uri Okrent 25ae621d01 Add sha1 and branch/tag text to the nodes (and give them some space)
Signed-off-by: Uri Okrent <uokrent@gmail.com>
2010-03-29 17:07:18 -07:00
Uri Okrent 5bb03ec8ba Make room for text in the node by providing each with an internal glyph
Moved the ellipse to the side of the node (and only half the width of
the node, and provide a method of addressing the internal ellipse's
center point rather than the center point of the entire node (for use by
the edges).

Signed-off-by: Uri Okrent <uokrent@gmail.com>
2010-03-29 17:07:18 -07:00
Uri Okrent a54a407762 Set background color the simple way with the background brush
This gets rid of the artifacts we were seeing when moving around the
scene.  It isn't a fancy gradient, but oh well...

Signed-off-by: Uri Okrent <uokrent@gmail.com>
2010-03-29 17:07:18 -07:00
Uri Okrent c5b30e3593 Add additional interesting info to Commits
They now contain the short sha1, the subject, a list of parents, a list
of tags, the author's name, and the author date.

Also added a __repr__ to Commit for sanity's sake

Signed-off-by: Uri Okrent <uokrent@gmail.com>
2010-03-29 17:07:18 -07:00
David Aguilar c96e0c1e14 treeview: add a graphical history viewer
This adds the cola.views.dag history browser.
This is the beginnings of a nice QGraphicsView
based tree browser.

Signed-off-by: David Aguilar <davvid@gmail.com>
2010-03-29 17:07:18 -07:00
David Aguilar 25abaf436a models: add a commits() method for traversing history
This adds a basic commit object and commits() method to
return a commit "objects" for use in history traversal.

Signed-off-by: David Aguilar <davvid@gmail.com>
2010-03-29 17:07:18 -07:00
David Aguilar 579360d384 Mention all git-cola contributors in the AUTHORS file
Signed-off-by: David Aguilar <davvid@gmail.com>
2010-03-29 17:06:08 -07:00
David Aguilar a3921f7c21 Use the new gettext infrastructure for i18n
This adds a cola.i18n module for doing gettext translations.
This does away with the qm-based translation system in favor
of a standard gettext setup.

We still monkey-patch QApplication.translate() with our
own custom translator.  The main difference is switching
from a Qt-specific QTranslator translation system to a
gettext-based one.

Signed-off-by: David Aguilar <davvid@gmail.com>
2010-03-29 17:06:02 -07:00
David Aguilar db70e338b8 gitcfg: Guard against moved or deleted .gitconfigs
Users can actively move or rename files so guard against
this by calling reset() whenever the cache key changes.
The original code calculated the path list once at
startup and did not ever recalculate it.  We now recalculate
the list whenever the stat info changes.

A subtle change here is that the old code would always
check $prefix/etc before checking /etc/gitconfig.
git always checks /etc/gitconfig regardless of $prefix
so we can remove that check.

Signed-off-by: David Aguilar <davvid@gmail.com>
2010-03-17 01:50:59 -07:00
David Aguilar 19db380842 gitcfg: Add a get_encoding() function
Returns gui.encoding, defaults to utf-8.

Signed-off-by: David Aguilar <davvid@gmail.com>
2010-03-17 01:38:39 -07:00
David Aguilar ff10d82de1 cola.gitcfg: Remove unused import
Signed-off-by: David Aguilar <davvid@gmail.com>
2010-03-17 01:37:35 -07:00
David Aguilar ee2d00ffc6 Makefile: Add 'tags' to .PHONY
Signed-off-by: David Aguilar <davvid@gmail.com>
2010-03-10 11:25:29 -08:00
David Aguilar e2526f60a4 gitcmds: Use rev-parse --symbolic-full-name
There's not much overhead in calling out to git now that
we use a stat cache on .git/HEAD.  The Python implementation
is still available as _read_git_head() and is still used
for the 'git init' case.

Signed-off-by: David Aguilar <davvid@gmail.com>
2010-03-10 11:25:29 -08:00
David Aguilar 0b2c607df2 utils: Add an is_win32() helper function
So we don't have to keep writing `if os.name in ('nt', 'dos')`.

Signed-off-by: David Aguilar <davvid@gmail.com>
2010-03-10 11:25:29 -08:00
David Aguilar ea498ede49 setup.py: Simplify by removing packages/package_data usage
Reusing the existing _app_path() is simpler.

Signed-off-by: David Aguilar <davvid@gmail.com>
2010-03-10 11:25:29 -08:00
David Aguilar 6ad4a0db46 Makefile: Add 'qm', 'pot', and 'mo' targets
The 'qm'-based i18n is currently used but we may be switching
to standard .pot and .mo files soon.  This provides the infrastructure
to get this going.

Signed-off-by: David Aguilar <davvid@gmail.com>
2010-03-10 11:25:29 -08:00
David Aguilar ab8796fc34 setup.py: Integrate build_pot, build_mo, and build_qm commands
Signed-off-by: David Aguilar <davvid@gmail.com>
2010-03-10 11:25:29 -08:00
David Aguilar 5d113a0614 setup.py: Check Python version using platform.python_version()
This is better than stringifying sys.version_info().

Signed-off-by: David Aguilar <davvid@gmail.com>
2010-03-10 11:25:29 -08:00
David Aguilar 15ca99f677 build: Add commands for buliding .pot, .mo, and .qm files
Signed-off-by: David Aguilar <davvid@gmail.com>
2010-03-10 11:25:29 -08:00
David Aguilar ae1aacc692 COPYRIGHT: List individual publication dates
Signed-off-by: David Aguilar <davvid@gmail.com>
2010-03-10 11:25:29 -08:00
David Aguilar 548f011930 Makefile: 'install' depends on 'all'
Signed-off-by: David Aguilar <davvid@gmail.com>
2010-02-27 04:25:14 -08:00
David Aguilar 3d97704688 views.status: Improve selection handling across updates
We now manage the selection across updates much more carefully.
When items are staged they are selected and their
diff is automatically shown.  Vice-versa for unstaged.

A group of selected items retains their selection across
category moves.  If an item moves into a new category,
we scroll the viewport to the item in that category.

Priority is given to the current, partially-staged diff
when interactively staging hunks.  Otherwise, we would
be breaking context by showing the unstaged diff and then
switching to the staged fragment.

Signed-off-by: David Aguilar <davvid@gmail.com>
2010-02-27 04:24:36 -08:00
David Aguilar cf5fdeb998 views.status: Use QTreeWidget.indentation() instead of magic numbers
Qt knows the size of the tree widget's indentation so use a derived
value instead of hard-coding the x-offset to the icon.

Signed-off-by: David Aguilar <davvid@gmail.com>
2010-02-27 04:17:55 -08:00
David Aguilar 5eed0c51cd views.status: Process click-to-un/stage on mouse release
Doing this on mouse press was causing an odd interaction with
the selection.  Defer until mouse release so that the selection
is applied before this event occurs.

Signed-off-by: David Aguilar <davvid@gmail.com>
2010-02-27 04:14:31 -08:00
David Aguilar 226a5ab1c2 views.status: Remember scrollbar positions across updates
Restore the vertical scrollbar's position when model updates occur.
This keeps the viewport from moving around too much.

Signed-off-by: David Aguilar <davvid@gmail.com>
2010-02-27 03:59:24 -08:00
David Aguilar c7799976e1 Use 'gvim' when the editor is 'vim'
Some users might have $EDITOR configured to 'vim'.
We should redirect these calls to 'gvim' to avoid
creating a zombie editor.

Signed-off-by: David Aguilar <davvid@gmail.com>
2010-02-23 17:04:48 -08:00
David Aguilar 9e06f3ec05 Honor the $VISUAL environment variable
$VISUAL defines the name of a visual editor and should be
used in favor of $EDITOR.

Signed-off-by: David Aguilar <davvid@gmail.com>
2010-02-23 16:43:39 -08:00
David Aguilar 607ea6ddc8 Makefile: Defer calculating $(COLA_VERSION)
Signed-off-by: David Aguilar <davvid@gmail.com>
2010-02-19 16:38:01 -08:00
David Aguilar 607a4f4d96 Makefile: Add tests to 'tags' target
Signed-off-by: David Aguilar <davvid@gmail.com>
2010-02-19 16:37:26 -08:00
David Aguilar 0816b99cfb darwin/name-tarball.py: Add support for snow leopard
Recognize '10.6' as snow_leopard.
Additionally, not knowing the mac platform shouldn't
prevent us from being able to name the tarball.

Signed-off-by: David Aguilar <davvid@gmail.com>
2010-02-12 23:51:32 -08:00
David Aguilar 08dea92585 darwin: Update README to mention .app workarounds
Testing .app bundles is non-trivial on PPC Tiger machines
so mention the testing workaround mentioned on the git-cola
mailing list.

Signed-off-by: David Aguilar <davvid@gmail.com>
2010-02-11 01:43:28 -08:00
David Aguilar 8fbd9e31f5 git: Pass --no-color when calling 'git log'
Users can specify 'git config color.ui always' in  git config
so pass --no-color to override the configuration.

Signed-off-by: David Aguilar <davvid@gmail.com>
2010-02-08 11:28:01 -08:00
David Aguilar 53e98570c4 controllers.selectcommits: Do not pass string to QObject.connect()
QObserver was recently reworked to not override QObject.connect().
When this happened it stopped automatically wrapping signal names
with SIGNAL().

Do the mainstream Qt thing and wrap the signal name with SIGNAL().

Fixes #42

Signed-off-by: David Aguilar <davvid@gmail.com>
2010-02-02 22:05:12 -08:00
David Aguilar 69c900775b win32: Add system-standard paths to create-installer.sh
Signed-off-by: David Aguilar <davvid@gmail.com>
2010-02-02 17:05:49 -08:00
David Aguilar 933d2f8a0a win32: Use a relative directory when building installer
Otherwise setup.py writes files to the wrong directory.

Fixes #40

Signed-off-by: David Aguilar <davvid@gmail.com>
2010-02-02 16:48:20 -08:00
David Aguilar 71d59c23dc win32: Remove colon from application name
The Windows installer suggested
"git-cola : a highly caffeinated git GUI" as the start-menu entry.
This is an invalid folder-name, and the installer subsequently
refuses to use it unless changed.

Fixes #41

Signed-off-by: David Aguilar <davvid@gmail.com>
2010-02-02 16:15:01 -08:00
David Aguilar 675674cc67 review mode: Allow reviewing the current branch
Review mode used to always find the merge-base with HEAD.
We now search for a tracked branch and then fallback to
'master' if no tracked branch exists.

This is consistent with the common topic-branch workflow.

Signed-off-by: David Aguilar <davvid@gmail.com>
2010-02-01 15:39:36 -08:00
David Aguilar 80205ea16f gitcmds: Fix uninitialized variable error
Signed-off-by: David Aguilar <davvid@gmail.com>
2010-02-01 15:35:23 -08:00
David Aguilar d129d25466 views.status: Add submodule menu actions
This adds menu entries for staging submodule changes and
launching git-cola inside the submodule.

Closes #17

Signed-off-by: David Aguilar <davvid@gmail.com>
2010-01-19 00:17:40 -08:00
David Aguilar 7c693f91d3 views.main: Use different context menus for submodules
We can now stage submodules and launch git-cola on a submodule
from within the submodule diff text context menu.

Signed-off-by: David Aguilar <davvid@gmail.com>
2010-01-18 23:32:47 -08:00
David Aguilar e268f4e926 models.main: Keep track of submodule paths
We now keep track of submodules as reported by diff-index.

Signed-off-by: David Aguilar <davvid@gmail.com>
2010-01-18 23:32:16 -08:00
David Aguilar 8dc7188d47 views.mainwindow: Use the 'Qt' module directly
We already imported 'Qt' from 'QtCore' so there's no
need to reference it via 'QtCore.Qt' when we can simply
say 'Qt' instead.

Signed-off-by: David Aguilar <davvid@gmail.com>
2010-01-18 23:31:24 -08:00
David Aguilar 1182bb8217 qobserver, views.main: Always use setPlainText()
setText() guesses the type based on content.
We *always* want plain text, so use setPlainText() instead.

Signed-off-by: David Aguilar <davvid@gmail.com>
2010-01-18 23:29:58 -08:00
David Aguilar 4d352f58b4 Use 'git diff --submodule' when available
The first steps towards better submodule support.

Signed-off-by: David Aguilar <davvid@gmail.com>
2010-01-18 23:12:16 -08:00
David Aguilar cf0f4be5d1 qt: Make it easier to resize QFlowLayoutWidgets
This adds a size policy so that QFlowLayoutWidgets are easier
to resize.  They are no longer constrained to the size of their
child widgets.

As a consequence, the "Actions" dock can now be resized to be
in its vertical orientation even when space is limited.

Signed-off-by: David Aguilar <davvid@gmail.com>
2010-01-17 13:07:00 -08:00
David Aguilar 85307ee9a1 mainwindow: Make the margin around the commit buttons larger
This adds some spacing above the Sign Off/Commit buttons.

Signed-off-by: David Aguilar <davvid@gmail.com>
2010-01-17 13:03:18 -08:00
David Aguilar d42b9dcafc views.mainwindow: Use a QFlowLayoutWidget for the actions dock
This adds a new box layout widget that changes its orientation when it
is resized.  This allows for a more flexible arrangement of the GUI.

Signed-off-by: David Aguilar <davvid@gmail.com>
2010-01-16 16:09:07 -08:00
David Aguilar aa29b28853 doc: Add Boris to the contributors list
Signed-off-by: David Aguilar <davvid@gmail.com>
2010-01-15 23:06:22 -08:00
Boris W e4b8dde05b mainwindow: Add alt-key shortcuts
This incorporates alt-key shortcuts into the Qt menu.

Signed-off-by: Boris W <boris@tetago.com>
Signed-off-by: David Aguilar <davvid@gmail.com>
2010-01-13 00:20:20 -08:00
David Aguilar ba564772c6 cola.git{cfg,cmds}: Use an os.stat() cache
We now use stat.st_mtime to tell us when the current branch or
git config data has changed.

Signed-off-by: David Aguilar <davvid@gmail.com>
2010-01-11 10:23:28 -08:00
David Aguilar 9b156099c6 doc: Do not mention GIT_DIFF_TOOL
The fact that difftool honors GIT_DIFF_TOOL is an implementation
detail and should never have been mentioned in the documentation.

Signed-off-by: David Aguilar <davvid@gmail.com>
2010-01-09 18:27:57 -08:00
David Aguilar 0668b4a380 difftool: Do not consider GIT_MERGE_TOOL when setting the difftool
We should have never supported both GIT_DIFF_TOOL and GIT_MERGE_TOOL.

Signed-off-by: David Aguilar <davvid@gmail.com>
2010-01-09 18:27:12 -08:00
Iulian Udrea 7458da3455 setup.py: Fix 'shutil' usage
Import the missing 'shutil' module and fix its misspelling.

Signed-off-by: Iulian Udrea <iulian@ubuntu.com>
Signed-off-by: David Aguilar <davvid@gmail.com>
2010-01-05 13:25:47 -08:00
David Aguilar 208960acc7 qobserver: Do not issue notifications for textfields in SLOT()
This simplifies the handling for text fields by removing the
_in_textfield and _in_callback state variables.

In order to avoid loopbacks we simply pass notify=False when
setting the model parameters for text fields in SLOT().

Signed-off-by: David Aguilar <davvid@gmail.com>
2010-01-04 17:38:23 -08:00
David Aguilar accff05a48 qobserver: Do not override QObject.connect()
This removes the hacky "always wrap connect the signal string in SIGNAL()"
connect() method implemented in QObserver.

Signed-off-by: David Aguilar <davvid@gmail.com>
2010-01-04 17:26:57 -08:00
David Aguilar a11af49e60 models.compare: Add missing 'describe()' method
This method was dropped when the compare data was factored
out of the main model.

Signed-off-by: David Aguilar <davvid@gmail.com>
2010-01-04 14:36:15 -08:00
David Aguilar 75185df70f models.base: Remove serialization methods
All serialization logic lives in cola.serializer so it can now
be removed from the model.

Signed-off-by: David Aguilar <davvid@gmail.com>
2010-01-04 11:49:55 -08:00
David Aguilar 6b5ce5f4bc controllers.bookmark: Use cola.serializer when saving bookmarks
Signed-off-by: David Aguilar <davvid@gmail.com>
2010-01-04 11:48:53 -08:00
David Aguilar 87c984e371 controllers.repobrowser: Use a BrowserModel when selecting files
We now use a consistent model when dealing with the repobrowser.

Signed-off-by: David Aguilar <davvid@gmail.com>
2010-01-04 11:48:27 -08:00
David Aguilar 548198abc6 models.observable: Add a serialization handler for ObservableModel
Signed-off-by: David Aguilar <davvid@gmail.com>
2010-01-04 11:46:09 -08:00
David Aguilar 464d651068 controllers: Use cola.serializer's clone() function
Signed-off-by: David Aguilar <davvid@gmail.com>
2010-01-04 11:45:36 -08:00
David Aguilar dd945441ed settings: Use cola.serializer for saving the SettingsModel
Signed-off-by: David Aguilar <davvid@gmail.com>
2010-01-04 10:57:20 -08:00
David Aguilar 7d3d60322b cola: Add cola.serializer for handling object serialization
This will contain the serialization functionality currently
coupled to the Model class.

Signed-off-by: David Aguilar <davvid@gmail.com>
2010-01-04 10:55:12 -08:00
David Aguilar 8388f7e4c6 cola: Add a cola.compat module for python2.4 compatibility
cola.compat provides the 'set' class for python2.4.

Signed-off-by: David Aguilar <davvid@gmail.com>
2010-01-04 10:25:22 -08:00
David Aguilar d50ea82596 COPYRIGHT: 2010 update
Signed-off-by: David Aguilar <davvid@gmail.com>
2010-01-04 10:22:40 -08:00
David Aguilar bab6aa62bf .gitignore: Consolidate subdirectory entries
A single file is easier to maintain.

Signed-off-by: David Aguilar <davvid@gmail.com>
2010-01-04 10:22:23 -08:00
David Aguilar 3f0e8bbdc9 cola: Move simplejson and jsonpickle modules to thirdparty/
Signed-off-by: David Aguilar <davvid@gmail.com>
2010-01-04 10:21:11 -08:00
David Aguilar 177eed86f3 gitcmds: Use plumbing to implement unstage_paths()
Fallback to git-update-index instead of git-add when git-reset
exits because we are on a branch that is yet to be born.

Signed-off-by: David Aguilar <davvid@gmail.com>
2010-01-02 12:14:04 -08:00
David Aguilar 3beabc69da doc: Use the phrase 'git GUI' when describing git-cola
Signed-off-by: David Aguilar <davvid@gmail.com>
2010-01-02 12:14:04 -08:00
David Aguilar 325857d317 doc: Remove '*' pattern from _templates/.gitignore
That's the wrong pattern to use because it prevents us from
using templates in the future.  If we start using our own Sphinx
templates later then we can remove .gitignore altogether.

Signed-off-by: David Aguilar <davvid@gmail.com>
2010-01-02 12:14:04 -08:00
David Aguilar 8525f78db2 doc: Remove unnecessary .gitignore in the _static/ directory
Signed-off-by: David Aguilar <davvid@gmail.com>
2010-01-02 12:14:03 -08:00
David Aguilar 1034abdfeb doc: Remove redundant bullet item from release notes
Signed-off-by: David Aguilar <davvid@gmail.com>
2010-01-02 12:14:03 -08:00
David Aguilar 042dee98d4 doc: Add 'core git hackers' to the 'Thanks to' section
Signed-off-by: David Aguilar <davvid@gmail.com>
2009-12-31 18:01:24 -08:00
David Aguilar fb3bd7ea6b doc: Add missing screenshots
Darn you .gitignore!

Signed-off-by: David Aguilar <davvid@gmail.com>
2009-12-31 17:58:41 -08:00
David Aguilar 4a926060c5 resources: Link to the Sphinx HTML documentation
Signed-off-by: David Aguilar <davvid@gmail.com>
2009-12-31 17:13:37 -08:00
David Aguilar 6ba1873a29 doc: Add Iulian Udrea to the contributors list
Signed-off-by: David Aguilar <davvid@gmail.com>
2009-12-24 16:57:48 -08:00
David Aguilar c67f76f202 doc: Update 1.4.1.2 release notes to mention bookmarks-at-startup
We now present a list of bookmarks when run outside of a git
repository so mention it in the release notes.

Signed-off-by: David Aguilar <davvid@gmail.com>
2009-12-24 16:52:11 -08:00
David Aguilar 0d82f79d86 doc: Add a page for the 'Actions' tool
Signed-off-by: David Aguilar <davvid@gmail.com>
2009-12-24 16:51:07 -08:00
David Aguilar 3a7f61bfc3 tools: Add a page for the 'Commit Message Editor' tool
Signed-off-by: David Aguilar <davvid@gmail.com>
2009-12-24 16:50:50 -08:00
David Aguilar ec45080d36 doc: Add a page for the 'Diff Viewer' tool
Signed-off-by: David Aguilar <davvid@gmail.com>
2009-12-24 16:50:22 -08:00
David Aguilar 038cc52d14 doc: Add an 'Actions' section to the status tool page
Signed-off-by: David Aguilar <davvid@gmail.com>
2009-12-24 16:49:49 -08:00
David Aguilar 89b04a638a doc: Refer to 'Diff Viewer' in the mainwindow page
The new interface says 'Diff Viewer' so we should use that.

Signed-off-by: David Aguilar <davvid@gmail.com>
2009-12-24 16:49:21 -08:00
David Aguilar 31d1ce81fd doc: Add a list of contributors to the git-cola intro page
Signed-off-by: David Aguilar <davvid@gmail.com>
2009-12-24 16:48:32 -08:00
David Aguilar f859aba466 doc: Update intro headings to say 'Contribute' and 'Git Cola?'
Signed-off-by: David Aguilar <davvid@gmail.com>
2009-12-24 16:47:48 -08:00
David Aguilar 7fe339e954 views.main: Change 'Undo Selection' to 'Undo Selected Lines'
This makes the diff viewer context menu entries more consistent.

Signed-off-by: David Aguilar <davvid@gmail.com>
2009-12-24 15:58:00 -08:00
David Aguilar 964d6eebc4 INSTALL: Mention the 'standalone' make variable
This adds an example of the 'standalone' make variable for use
by packagers.

Signed-off-by: David Aguilar <davvid@gmail.com>
2009-12-24 15:57:25 -08:00
David Aguilar d0f1ab5fe5 Merge branch 'show-bookmarks-at-startup'
* show-bookmarks-at-startup:
  views.startup: Always create bookmark widgets
  Add bookmark list to startup window.
2009-12-24 02:20:27 -08:00
David Aguilar 7e5d58893c doc: Update 1.4.1.2 release notes
Mention the inotify non-existent directories fix.
Mention the 'make standalone=true' variable.

Signed-off-by: David Aguilar <davvid@gmail.com>
2009-12-24 01:55:14 -08:00
David Aguilar d34ae77e80 doc: Fix formatting for the 'cola.showoutput' release note
Signed-off-by: David Aguilar <davvid@gmail.com>
2009-12-24 01:53:58 -08:00
David Aguilar 9bdc952237 views.startup: Always create bookmark widgets
We now unconditionally create bookmark widgets and
hide them when no bookmarks exist.

This also modifies the code to use spaces instead of tabs.

Signed-off-by: David Aguilar <davvid@gmail.com>
2009-12-24 01:37:28 -08:00
David Aguilar 94ce5dd92c Update to the latest jsonpickle
I'd rather not keep a copy of this here but py2app and the
win32 installer both need them.

Signed-off-by: David Aguilar <davvid@gmail.com>
2009-12-24 00:57:02 -08:00
David Aguilar d42fd033ef Update to the latest simplejson
I'd rather not keep a copy of this here but py2app and the
win32 installer both need them.

Signed-off-by: David Aguilar <davvid@gmail.com>
2009-12-24 00:57:02 -08:00
David Aguilar d4d68604f5 Makefile: Exclude jsonpickle and simplejson from nosetests
Signed-off-by: David Aguilar <davvid@gmail.com>
2009-12-24 00:54:50 -08:00
David Aguilar 6466941893 inotify: Don't attempt to watch non-existant directories
This handles the case where directories are deleted
but not yet committed.

Signed-off-by: David Aguilar <davvid@gmail.com>
2009-12-24 00:14:46 -08:00
David Aguilar 3b562335ae Makefile: Support setup.py's --standalone option
This allows the Makefile to pass --standalone to setup.py
when the 'standalone' make variable is supplied.

    make standalone=1 install

Signed-off-by: David Aguilar <davvid@gmail.com>
2009-12-23 23:43:15 -08:00
David Aguilar fb3445a09f setup.py: Add --standalone to avoid installing 3rdparty libs
Signed-off-by: David Aguilar <davvid@gmail.com>
2009-12-23 23:40:16 -08:00
David Aguilar fc1bdccb9f win32/create-installer.sh: Simplify installation
There's no need to check for meta/ReleaseNotes.

Signed-off-by: David Aguilar <davvid@gmail.com>
2009-12-23 23:37:46 -08:00
Daniel King 7cdcc37bd4 Add bookmark list to startup window. 2009-12-24 15:37:12 +08:00
David Aguilar 9139be9be9 win32/create-installer.sh: Simplify setup.py usage
There's no need to pass --root to setup.py.

Signed-off-by: David Aguilar <davvid@gmail.com>
2009-12-23 23:36:44 -08:00
David Aguilar 9552c0f853 setup.py: Remove unused _run_cmd() function
Signed-off-by: David Aguilar <davvid@gmail.com>
2009-12-23 23:33:35 -08:00
David Aguilar d2e0a7e499 MANIFEST.in: Don't include forked jsonpickle
Signed-off-by: David Aguilar <davvid@gmail.com>
2009-12-23 23:33:01 -08:00
David Aguilar 7b9adcee4b .gitignore: Add debian/ entries
Signed-off-by: David Aguilar <davvid@gmail.com>
2009-12-22 11:40:31 -08:00
David Aguilar d63a636d79 doc/Makefile: Remove all of _build/ when cleaning
Signed-off-by: David Aguilar <davvid@gmail.com>
2009-12-22 01:24:38 -08:00
David Aguilar a583324ff1 doc: Remove intermediate 'release notes' page
We don't need no stinkin subdir.

Signed-off-by: David Aguilar <davvid@gmail.com>
2009-12-22 01:24:09 -08:00
David Aguilar c611996663 doc/.gitignore: Move _build exclude to top-level doc directory
Signed-off-by: David Aguilar <davvid@gmail.com>
2009-12-22 00:17:42 -08:00
David Aguilar b82ca69d69 Makefile: Don't unnecessarily set DESTDIR
Otherwise we break naive config.mak with prefix=~/subdir.

Signed-off-by: David Aguilar <davvid@gmail.com>
2009-12-22 00:07:41 -08:00
David Aguilar 81d40cbdac doc: Update 1.4.1.2 release notes
We now mention the resized remote dialogs and the
the new 'make dist' target.

Signed-off-by: David Aguilar <davvid@gmail.com>
2009-12-21 22:46:20 -08:00
David Aguilar a3d0080bb3 doc: Add git-cola-1.4.1.2 release notes
Signed-off-by: David Aguilar <davvid@gmail.com>
2009-12-21 22:42:10 -08:00
David Aguilar d803b497cc doc: Add a "page" header to relnotes/index
Signed-off-by: David Aguilar <davvid@gmail.com>
2009-12-21 22:40:54 -08:00
David Aguilar ffaf968bf2 views.remote: Make the 'remote' dialog larger by default
Signed-off-by: David Aguilar <davvid@gmail.com>
2009-12-21 22:28:51 -08:00
David Aguilar 7a23888cb6 doc: Mention drag&drop and GUI layouts in 'mainwindow'
Signed-off-by: David Aguilar <davvid@gmail.com>
2009-12-21 22:24:45 -08:00
David Aguilar 3439b0e6ce doc: Add a 'tools' page and link 'status' beneath it
Signed-off-by: David Aguilar <davvid@gmail.com>
2009-12-21 22:24:02 -08:00
David Aguilar 828c715d36 doc: Remove unneeded sourcecode section
Signed-off-by: David Aguilar <davvid@gmail.com>
2009-12-21 22:23:10 -08:00
David Aguilar e9463b2307 views.status: Add 'Remove Unstaged Edits' for staged items
Signed-off-by: David Aguilar <davvid@gmail.com>
2009-12-19 16:30:49 -08:00
David Aguilar 618cf878a4 views.status: Add 'Remove Unstaged Edits'
This makes adds a new 'remove unstaged edits'
action and makes the language consistent in the
existing uncommited edits action.

Signed-off-by: David Aguilar <davvid@gmail.com>
2009-12-19 16:16:15 -08:00
David Aguilar 3a9f4b92f9 doc/intro: Add a 'Running git-cola' section
Signed-off-by: David Aguilar <davvid@gmail.com>
2009-12-18 13:17:23 -08:00
David Aguilar 2f15ffa27b Makefile: Add 'dist' target
The 'dist' target generates a tarball.

Signed-off-by: David Aguilar <davvid@gmail.com>
2009-12-18 12:57:45 -08:00
David Aguilar b69bdc4868 setup: Support a 'version' stamp file in dist tarballs
Signed-off-by: David Aguilar <davvid@gmail.com>
2009-12-18 12:57:10 -08:00
David Aguilar 0567515572 main: Don't run git update-index on git cola version
Signed-off-by: David Aguilar <davvid@gmail.com>
2009-12-18 12:55:39 -08:00
David Aguilar 8c6478b5f3 version: Trivial style cleanup
Signed-off-by: David Aguilar <davvid@gmail.com>
2009-12-18 12:47:50 -08:00
David Aguilar 8bea3ce158 doc: Break out userguide into intro, mainwindow, shortcuts
Signed-off-by: David Aguilar <davvid@gmail.com>
2009-12-18 15:53:24 -08:00
David Aguilar a48717e129 Makefile: Allow local customizations via config.mak
We can now keep around custom settings for Makefile
variables by using a 'config.mak' file.

Signed-off-by: David Aguilar <davvid@gmail.com>
2009-12-18 15:33:07 -08:00
David Aguilar 5eae81ce4d Makefile: Pass $(prefix) to the doc targets
Signed-off-by: David Aguilar <davvid@gmail.com>
2009-12-18 15:31:48 -08:00
David Aguilar a902886dfe .gitignore: Remove old/obsolete entries
Signed-off-by: David Aguilar <davvid@gmail.com>
2009-12-18 15:31:20 -08:00
David Aguilar e62d16db14 views.status: Add missing 'utils' import
Signed-off-by: David Aguilar <davvid@gmail.com>
2009-12-14 15:20:16 -08:00
David Aguilar 5d7cb0fa39 releases: Mention the desktop entry name change
Signed-off-by: David Aguilar <davvid@gmail.com>
2009-12-11 21:49:30 -08:00
David Aguilar 6af5d67683 doc: Rearrange section titles to improve the table of contents
This rearranges the table of contents so that things are easier
to find.  Notably, the release notes section now has a maxdepth
setting to prevent showing details about each release
on the index page.

Signed-off-by: David Aguilar <davvid@gmail.com>
2009-12-11 21:36:51 -08:00
David Aguilar dbfa6ac0c2 doc: Fix section titles to use correct underline
Signed-off-by: David Aguilar <davvid@gmail.com>
2009-12-11 21:34:43 -08:00
David Aguilar f1a7d53f78 doc: Add git-cola-1.4.1.1 release notes
Signed-off-by: David Aguilar <davvid@gmail.com>
2009-12-11 11:58:09 -08:00
David Aguilar 0592f94328 controllers.remote: Fix indentation error when pushing
The code to warn users about pushing new branches was
accidentally outdented.

Signed-off-by: David Aguilar <davvid@gmail.com>
2009-12-11 10:55:16 -08:00
David Aguilar 91196651d2 Merge commit 'mathstuf/master'
* commit 'mathstuf/master': (21 commits)
  More descriptive name for the menu entry
  models.gitrepo: Use NULL to delimit fields instead of slash
  views.repo: Add a shortcut for closing the classic widget
  qtutils: Add add_close_action()
  gitcmds: Remove the _use_patience variable
  version: Cache the output of 'git version'
  gitcmds: Use cola.version for patience
  version: Speed up version checks by using a cache
  Make our diffs more patient -- patience is a virtue
  qtutils: Do not re-translate strings
  cola.app: Guard against QString for PyQt4.3 compatibility
  doc: Mention push behavior in the release notes
  controllers.remote: Warn when creating a new branch
  INSTALL: Update download urls
  gitcmds: Fix invalid signature for merge_message()
  models.main: Save window settings by default
  views.mainwindow: Use a nicer default dock arrangement
  views.mainwindow: Use a larger default window size
  doc: Do not mention unused cola.geometry variable
  doc: Document the 'git cola --classic' option
  ...
2009-12-09 23:34:44 -08:00
Ben Boeckel 5455f1e682 More descriptive name for the menu entry 2009-12-10 01:06:33 -05:00
David Aguilar 8178833b4c models.gitrepo: Use NULL to delimit fields instead of slash
Signed-off-by: David Aguilar <davvid@gmail.com>
2009-12-10 01:06:33 -05:00
David Aguilar 096cd5c8be views.repo: Add a shortcut for closing the classic widget
Signed-off-by: David Aguilar <davvid@gmail.com>
2009-12-10 01:06:33 -05:00
David Aguilar c3995e7399 qtutils: Add add_close_action()
add_close_action() adds a Ctrl+w shortcut for closing a widget.

Signed-off-by: David Aguilar <davvid@gmail.com>
2009-12-10 01:06:33 -05:00
David Aguilar 1b7198c8ce gitcmds: Remove the _use_patience variable
We cache the output of 'git version' now so there's no need to
store the _use_patience calculation.

Signed-off-by: David Aguilar <davvid@gmail.com>
2009-12-10 01:06:33 -05:00
David Aguilar 0162327e3f version: Cache the output of 'git version'
Signed-off-by: David Aguilar <davvid@gmail.com>
2009-12-10 01:06:33 -05:00
David Aguilar 9b332ca960 gitcmds: Use cola.version for patience
This prevents running 'git version' everytime we run 'git diff'.

Signed-off-by: David Aguilar <davvid@gmail.com>
2009-12-10 01:06:33 -05:00
David Aguilar 9e2e175e94 version: Speed up version checks by using a cache
Signed-off-by: David Aguilar <davvid@gmail.com>
2009-12-10 01:06:32 -05:00
Uri Okrent 31980b4ab4 Make our diffs more patient -- patience is a virtue
Make --patience the default for diffs produced by cola, if the git
version supports it (that is the user's git client is at least 1.6.2).

Maybe this should be controlled by a configuration somewhere, but I'm
too lazy to make that work now. Also, git config doesn't seem to mention
any existing config for controlling whether or not diff is patience by
default so it doesn't seem like cola should be the thing deciding that.
Is there any downside to always using patience diff?

Signed-off-by: Uri Okrent <uokrent@gmail.com>
2009-12-10 01:06:32 -05:00
David Aguilar 40d52deba1 qtutils: Do not re-translate strings
This avoids calling QApplication.translate()
on a string that has already been translated.

Closes #34

Signed-off-by: David Aguilar <davvid@gmail.com>
2009-12-10 01:06:32 -05:00
David Aguilar 8e0ff16340 cola.app: Guard against QString for PyQt4.3 compatibility
Older versions of PyQt4 do not accept QStrings in calls
to QApplication.translate().  Guard against this situation
by wrapping text to translate in str().

Closes #34

Signed-off-by: David Aguilar <davvid@gmail.com>
2009-12-10 01:06:32 -05:00
David Aguilar 20b7cf4302 doc: Mention push behavior in the release notes
Signed-off-by: David Aguilar <davvid@gmail.com>
2009-12-10 01:06:32 -05:00
David Aguilar 86e8abbba8 controllers.remote: Warn when creating a new branch
Closes #35

Signed-off-by: David Aguilar <davvid@gmail.com>
2009-12-10 01:06:32 -05:00
David Aguilar c79c25d960 INSTALL: Update download urls
Signed-off-by: David Aguilar <davvid@gmail.com>
2009-12-10 01:06:32 -05:00
David Aguilar 7007ce8126 gitcmds: Fix invalid signature for merge_message()
We forgot to remove the 'self' argument when this got
factored out into the gitcmds module.

Closes #36

Signed-off-by: David Aguilar <davvid@gmail.com>
2009-12-10 01:06:32 -05:00
David Aguilar 712c5066f3 models.main: Save window settings by default
It's the user-friendly thing to do.

Signed-off-by: David Aguilar <davvid@gmail.com>
2009-12-10 01:06:32 -05:00
David Aguilar f323a00905 views.mainwindow: Use a nicer default dock arrangement
This adds the missing statusdockwidget and moves things
around so that the diff is on the bottom and the top
has an arrangement of [commit, status, actions].

Signed-off-by: David Aguilar <davvid@gmail.com>
2009-12-10 01:06:32 -05:00
David Aguilar 63a9ab32e2 views.mainwindow: Use a larger default window size
The Fibonacci ratio is quite nice.
2009-12-10 01:06:32 -05:00
David Aguilar 08dbc128ce doc: Do not mention unused cola.geometry variable
Signed-off-by: David Aguilar <davvid@gmail.com>
2009-12-10 01:06:32 -05:00
David Aguilar a20c7f5083 doc: Document the 'git cola --classic' option
Signed-off-by: David Aguilar <davvid@gmail.com>
2009-12-10 01:06:32 -05:00
David Aguilar 5cefaa08f0 models.gitrepo: Use NULL to delimit fields instead of slash
Signed-off-by: David Aguilar <davvid@gmail.com>
2009-12-08 00:07:53 -08:00
David Aguilar 31a79a9840 views.repo: Add a shortcut for closing the classic widget
Signed-off-by: David Aguilar <davvid@gmail.com>
2009-12-08 00:03:32 -08:00
David Aguilar 60b29e6d68 qtutils: Add add_close_action()
add_close_action() adds a Ctrl+w shortcut for closing a widget.

Signed-off-by: David Aguilar <davvid@gmail.com>
2009-12-08 00:03:20 -08:00
David Aguilar 42c2db8d17 gitcmds: Remove the _use_patience variable
We cache the output of 'git version' now so there's no need to
store the _use_patience calculation.

Signed-off-by: David Aguilar <davvid@gmail.com>
2009-12-07 23:55:30 -08:00
David Aguilar d6a84ac8bd version: Cache the output of 'git version'
Signed-off-by: David Aguilar <davvid@gmail.com>
2009-12-07 23:54:23 -08:00
David Aguilar d2d70ff708 gitcmds: Use cola.version for patience
This prevents running 'git version' everytime we run 'git diff'.

Signed-off-by: David Aguilar <davvid@gmail.com>
2009-12-07 00:24:28 -08:00
David Aguilar 7a1760e0f8 version: Speed up version checks by using a cache
Signed-off-by: David Aguilar <davvid@gmail.com>
2009-12-07 00:16:15 -08:00
Uri Okrent cb070daf2c Make our diffs more patient -- patience is a virtue
Make --patience the default for diffs produced by cola, if the git
version supports it (that is the user's git client is at least 1.6.2).

Maybe this should be controlled by a configuration somewhere, but I'm
too lazy to make that work now. Also, git config doesn't seem to mention
any existing config for controlling whether or not diff is patience by
default so it doesn't seem like cola should be the thing deciding that.
Is there any downside to always using patience diff?

Signed-off-by: Uri Okrent <uokrent@gmail.com>
2009-12-06 23:57:44 -08:00
David Aguilar 8b08a4dea6 qtutils: Do not re-translate strings
This avoids calling QApplication.translate()
on a string that has already been translated.

Closes #34

Signed-off-by: David Aguilar <davvid@gmail.com>
2009-12-04 16:03:08 -08:00
David Aguilar 320df6bd0a cola.app: Guard against QString for PyQt4.3 compatibility
Older versions of PyQt4 do not accept QStrings in calls
to QApplication.translate().  Guard against this situation
by wrapping text to translate in str().

Closes #34

Signed-off-by: David Aguilar <davvid@gmail.com>
2009-12-04 03:31:46 -08:00
David Aguilar 99d0bb064e doc: Mention push behavior in the release notes
Signed-off-by: David Aguilar <davvid@gmail.com>
2009-12-03 22:19:02 -08:00
David Aguilar 74c2ee9738 controllers.remote: Warn when creating a new branch
Closes #35

Signed-off-by: David Aguilar <davvid@gmail.com>
2009-12-03 22:16:36 -08:00
David Aguilar 4dfbf66e83 INSTALL: Update download urls
Signed-off-by: David Aguilar <davvid@gmail.com>
2009-12-03 21:27:40 -08:00
David Aguilar 57850301f9 gitcmds: Fix invalid signature for merge_message()
We forgot to remove the 'self' argument when this got
factored out into the gitcmds module.

Closes #36

Signed-off-by: David Aguilar <davvid@gmail.com>
2009-12-03 19:53:16 -08:00
David Aguilar de32d16dc7 models.main: Save window settings by default
It's the user-friendly thing to do.

Signed-off-by: David Aguilar <davvid@gmail.com>
2009-12-02 23:29:22 -08:00
David Aguilar e2cf5f0a53 views.mainwindow: Use a nicer default dock arrangement
This adds the missing statusdockwidget and moves things
around so that the diff is on the bottom and the top
has an arrangement of [commit, status, actions].

Signed-off-by: David Aguilar <davvid@gmail.com>
2009-12-02 23:23:39 -08:00
David Aguilar 53870294b6 views.mainwindow: Use a larger default window size
The Fibonacci ratio is quite nice.
2009-12-02 23:23:38 -08:00
David Aguilar cc0b70bfa3 doc: Do not mention unused cola.geometry variable
Signed-off-by: David Aguilar <davvid@gmail.com>
2009-12-02 23:23:38 -08:00
David Aguilar 188bba9f0b doc: Document the 'git cola --classic' option
Signed-off-by: David Aguilar <davvid@gmail.com>
2009-12-02 23:23:38 -08:00
Ben Boeckel 3c5463efb5 Don't ship forked simplejson 2009-12-01 19:15:03 -05:00
David Aguilar c6d6c32edf views.main: Use the clone callback from gitcmds
Signed-off-by: David Aguilar <davvid@gmail.com>
2009-11-30 01:33:47 -08:00
David Aguilar 73d784605a views.main: Remove unused class attributes
The widget refactor eliminated these.  Less is more.

Signed-off-by: David Aguilar <davvid@gmail.com>
2009-11-30 01:33:02 -08:00
David Aguilar 43a3379bb9 views.main: Use gitcmds in the 'browse branch' callbacks
Signed-off-by: David Aguilar <davvid@gmail.com>
2009-11-25 21:09:15 -08:00
David Aguilar 596a587847 gitcmds: Add a 'split' parameter to all_refs()
Passing split=True causes all_refs() to return three separate
local_branches, remote_branches, and tags lists.

all_refs() now returns a single concatenated list by default.

Signed-off-by: David Aguilar <davvid@gmail.com>
2009-11-25 21:08:18 -08:00
David Aguilar 848623ca90 models.main: Use the config object for gui.diffcontext.
Signed-off-by: David Aguilar <davvid@gmail.com>
2009-11-25 21:05:41 -08:00
David Aguilar d35f6087a2 models.main: Remove unused commit message methods
These live in the commands module now.

Signed-off-by: David Aguilar <davvid@gmail.com>
2009-11-25 21:02:03 -08:00
David Aguilar c84566c7bf views.main: Simplify load_template()
It simply signals.

Signed-off-by: David Aguilar <davvid@gmail.com>
2009-11-25 20:49:11 -08:00
David Aguilar d33c10a3b4 qtutils: Death to whitespace
Signed-off-by: David Aguilar <davvid@gmail.com>
2009-11-25 20:35:21 -08:00
David Aguilar 889b1dd559 gitcmds: Use rstrip() when reading .git/HEAD
Every little bit helps.

Signed-off-by: David Aguilar <davvid@gmail.com>
2009-11-25 20:34:38 -08:00
David Aguilar c78a04f37d commands: Add LoadCommitTemplate
LoadCommitTempate loads the commit message specified in commit.template.

Signed-off-by: David Aguilar <davvid@gmail.com>
2009-11-25 20:34:03 -08:00
David Aguilar b3b84194b2 gitcfg: Fix git-config usage for git 1.6.2
Certain versions of git are sensitive to the order of options
passed to the git-config command.  This ensures that --list
is passed last.

Signed-off-by: David Aguilar <davvid@gmail.com>
2009-11-25 15:51:45 -08:00
David Aguilar a39ba8d9af doc: Mention startup optimizations in the release notes
Signed-off-by: David Aguilar <davvid@gmail.com>
2009-11-22 15:31:46 -08:00
David Aguilar 52dbb4e3bf doc: Use backticks and rephrase release notes where appropriate
Signed-off-by: David Aguilar <davvid@gmail.com>
2009-11-22 15:30:05 -08:00
David Aguilar f6869a59c0 gitcmds: Simplify cache usage in current_branch()
current_branch() used a stat cache which was not very reliable.
We now use the raw content of .git/HEAD as the cache key.
This is simpler and more robust than a stat() cache.

Signed-off-by: David Aguilar <davvid@gmail.com>
2009-11-21 20:43:03 -08:00
David Aguilar 599750b4ff gitcfg: Simplify cache usage
The stat cache is probably not very portable and doesn't lend
itself well to testing so bail on it.  We now use the raw
content of each config file as its cache key.

Signed-off-by: David Aguilar <davvid@gmail.com>
2009-11-21 20:41:51 -08:00
David Aguilar 1b8e0e9b7d cola: Make static factory functions consistent in implementation
Signed-off-by: David Aguilar <davvid@gmail.com>
2009-11-21 20:39:22 -08:00
David Aguilar 9d11d1f545 test: Chain shell() commands
This ensures that all git commands run to completion.

Signed-off-by: David Aguilar <davvid@gmail.com>
2009-11-21 18:55:02 -08:00
David Aguilar 0607bce439 gitcmds: Optimize startup time by adding all_refs()
all_refs() finds tags and branches in one shot.
This avoids two calls to the git command.

Signed-off-by: David Aguilar <davvid@gmail.com>
2009-11-21 18:52:57 -08:00
David Aguilar 087498ac49 models.main: Remove redundant calls to git-config during startup
Signed-off-by: David Aguilar <davvid@gmail.com>
2009-11-21 13:59:21 -08:00
David Aguilar 6e8f4ab6ff gitcmds: Use st_mtime as the cache key
Otherwise simply reading .git/HEAD invalidates the
current_branch() cache.

Signed-off-by: David Aguilar <davvid@gmail.com>
2009-11-21 13:59:13 -08:00
David Aguilar 17477a2609 gitcfg: Use st_mtime as the cache key
Otherwise the act of reading the configs makes the
stat cache invalid.

Signed-off-by: David Aguilar <davvid@gmail.com>
2009-11-21 05:05:08 -08:00
David Aguilar 6e97c7f2e0 cola: Defer loading the classic view
This removes the classic dock widget.  There's a few reasons
why it's nice to have the classic view in its own window.

Signed-off-by: David Aguilar <davvid@gmail.com>
2009-11-21 04:13:38 -08:00
David Aguilar 49bf148b53 commands: Make model access consistent by using self.model
This is a trivial style cleanup to make the command module
consistent in how it accesses the model object.

Signed-off-by: David Aguilar <davvid@gmail.com>
2009-11-20 21:10:16 -08:00
David Aguilar 171dd0c62a models.main: Remove git_repo_path()
This removes the model's git_repo_path().
Callers now use git.git_path() instead.

Signed-off-by: David Aguilar <davvid@gmail.com>
2009-11-20 21:08:13 -08:00
David Aguilar e3d861df3f gitcmds: Update docstring for untracked_files()
Signed-off-by: David Aguilar <davvid@gmail.com>
2009-11-20 20:58:54 -08:00
David Aguilar 1f1420ab6d doc: Update git-cola-1.4.1 release notes
Added portability and developer topics to the upcoming
release notes.

Signed-off-by: David Aguilar <davvid@gmail.com>
2009-11-20 20:52:37 -08:00
David Aguilar 6cc9b54f54 test: Add a testcase for gitcmds.merge_message_path()
Signed-off-by: David Aguilar <davvid@gmail.com>
2009-11-20 20:45:45 -08:00
David Aguilar 039267c574 gitcmds: Factor out abort_merge() and related funcitons
This moves abort_merge(), merge_message_path(), and
merge_message() into the gitcmds module.

Signed-off-by: David Aguilar <davvid@gmail.com>
2009-11-20 20:06:11 -08:00
David Aguilar 2af18c8742 gitcmds: Use git.git_path() to find refs/heads
Signed-off-by: David Aguilar <davvid@gmail.com>
2009-11-20 19:15:49 -08:00
David Aguilar 942b59fe5b gitcmds: Remove unused reset_helper()
Signed-off-by: David Aguilar <davvid@gmail.com>
2009-11-20 19:13:24 -08:00
David Aguilar 20bd747b51 test: Add a testcase for gitcmds.tag_list()
Signed-off-by: David Aguilar <davvid@gmail.com>
2009-11-20 18:42:35 -08:00
David Aguilar 27042f7061 test: Add a testcase for gitcmds.untracked_files()
Signed-off-by: David Aguilar <davvid@gmail.com>
2009-11-20 17:17:47 -08:00
David Aguilar 545cc79f54 gitcmds: Remove unused 'cola' import
Signed-off-by: David Aguilar <davvid@gmail.com>
2009-11-20 16:56:05 -08:00
David Aguilar 1deadb4bd8 gitcmds: Simplify default_remote()
Signed-off-by: David Aguilar <davvid@gmail.com>
2009-11-20 16:49:03 -08:00
David Aguilar 14ce205c7f gitcmds: Remove unused corresponding_remote_ref()
Signed-off-by: David Aguilar <davvid@gmail.com>
2009-11-20 16:49:02 -08:00
David Aguilar 47303810ed gitcmds: Use the GitConfig object for diff.context
Signed-off-by: David Aguilar <davvid@gmail.com>
2009-11-20 16:49:02 -08:00
David Aguilar 4a205d6a98 controllers.compare: Handle tracked_branch() returning None
Signed-off-by: David Aguilar <davvid@gmail.com>
2009-11-20 16:49:02 -08:00
David Aguilar 3a4f350444 commands: Use the GitConfig object for diff.context
Signed-off-by: David Aguilar <davvid@gmail.com>
2009-11-20 16:48:54 -08:00
David Aguilar ffdb24164c gitcmds: Test and simplify tracked_branch()
Signed-off-by: David Aguilar <davvid@gmail.com>
2009-11-20 16:45:44 -08:00
David Aguilar bd0d3efc44 test: Add a testcase for default values from GitConfig.get()
Signed-off-by: David Aguilar <davvid@gmail.com>
2009-11-20 16:09:30 -08:00
David Aguilar fe53a54d7f test: Add a testcase for bool values from GitConfig.get()
Signed-off-by: David Aguilar <davvid@gmail.com>
2009-11-20 16:07:28 -08:00
David Aguilar 8485311a4f test: Add a testcase for int values from GitConfig.get()
Signed-off-by: David Aguilar <davvid@gmail.com>
2009-11-20 16:05:57 -08:00
David Aguilar f85b184043 doc: Document the gitcfg module
Signed-off-by: David Aguilar <davvid@gmail.com>
2009-11-20 16:04:02 -08:00
David Aguilar 9296c491e8 test: Add a test module for the gitcfg module
Signed-off-by: David Aguilar <davvid@gmail.com>
2009-11-20 16:01:45 -08:00
David Aguilar 39d7157abe gitcfg: Simplify get() by looping over candidate dictionaries
Signed-off-by: David Aguilar <davvid@gmail.com>
2009-11-20 16:01:31 -08:00
David Aguilar b5856d7e02 gitcmds: Use the static GitConfig instance in default_remote()
This modifies default_remote() to use the static GitConfig instance.
The old version did not always return the right value so this also
adds a test case to ensure correctness.

Signed-off-by: David Aguilar <davvid@gmail.com>
2009-11-20 15:55:42 -08:00
David Aguilar 870f240873 test: Reset the gitcfg instance in GitRepositoryTestCase.setUp()
Signed-off-by: David Aguilar <davvid@gmail.com>
2009-11-20 15:49:59 -08:00
David Aguilar b20e3e6f4c gitcmds: Use git.git_path() in current_branch()
Signed-off-by: David Aguilar <davvid@gmail.com>
2009-11-20 15:48:50 -08:00
David Aguilar 9b15b1a083 cola: Add a gitcfg module for interacting with git-config files
Signed-off-by: David Aguilar <davvid@gmail.com>
2009-11-20 15:48:27 -08:00
David Aguilar 36af28aff6 test: Reset the gitcmd instance in GitRepositoryTestCase.setUp()
Signed-off-by: David Aguilar <davvid@gmail.com>
2009-11-20 15:44:17 -08:00
David Aguilar 83748ea84a gitcmd: Add a git_path() method for finding .git-relative paths
Signed-off-by: David Aguilar <davvid@gmail.com>
2009-11-20 15:43:10 -08:00
David Aguilar 465005278b test: Add a test case for gitcmd.branch_list(remote=True)
Signed-off-by: David Aguilar <davvid@gmail.com>
2009-11-20 11:03:51 -08:00
David Aguilar 446a0bdb47 test: Add a test case for gitcmds.branch_list(remote=False)
Signed-off-by: David Aguilar <davvid@gmail.com>
2009-11-20 11:00:37 -08:00
David Aguilar c4cc7474aa test: Add a test for the model's 'tags' attribute
Signed-off-by: David Aguilar <davvid@gmail.com>
2009-11-20 10:59:29 -08:00
David Aguilar 95b976dd4c gitcmd: Use the entire stat object as the cache key in current_branch()
Signed-off-by: David Aguilar <davvid@gmail.com>
2009-11-20 10:59:13 -08:00
David Aguilar e391197488 models.main: Remove unused 'filename' method
Signed-off-by: David Aguilar <davvid@gmail.com>
2009-11-20 10:58:31 -08:00
David Aguilar 8dbcb13185 test: Default 'commit' to True in GitRepositoryTestCase
Signed-off-by: David Aguilar <davvid@gmail.com>
2009-11-20 10:57:32 -08:00
David Aguilar d48857d8e8 views.status: Avoid setHeaderHidden() altogether
We could use it, but the old way of doing it works too.
This simplifies PyQt 4.3 compatibility.

Signed-off-by: David Aguilar <davvid@gmail.com>
2009-11-20 01:57:08 -08:00
David Aguilar 923349b5f1 cola: Support PyQt 4.3
git-cola originally support PyQt 4.3.x.
We lost compatibility with the recent UI rework.
This makes git-cola work on PyQt 4.3.x again by simply
avoiding some of the newer feature.

Signed-off-by: David Aguilar <davvid@gmail.com>
2009-11-20 01:54:27 -08:00
David Aguilar 7f97d2fe74 models.gitrepo: Use a compatibility check for PyQt 4.3.x
This makes the tests pass on older versions of PyQt.

Signed-off-by: David Aguilar <davvid@gmail.com>
2009-11-20 01:54:26 -08:00
David Aguilar c374442e83 gitcmds: Fix for-each-ref usage for git-1.5.x
Signed-off-by: David Aguilar <davvid@gmail.com>
2009-11-20 01:54:26 -08:00
David Aguilar a82bb01e79 test: Add a test_path() helper for use in git repository tests
Signed-off-by: David Aguilar <davvid@gmail.com>
2009-11-20 01:54:25 -08:00
David Aguilar 04a5337b1d test: Skip the ColaApplication test when PyQt4 < 4.4
Signed-off-by: David Aguilar <davvid@gmail.com>
2009-11-20 01:54:25 -08:00
David Aguilar 9567ded3d0 test: Remove embedded utf-8 characters in test_cola_app
Use unichr() instead.

Signed-off-by: David Aguilar <davvid@gmail.com>
2009-11-20 01:54:25 -08:00
David Aguilar c9dee3d3f1 app: Add an instance() function to return the global QApplication
Signed-off-by: David Aguilar <davvid@gmail.com>
2009-11-20 01:54:25 -08:00
David Aguilar 8ad47e16a5 test: Test the model's 'remotes' attribute
Signed-off-by: David Aguilar <davvid@gmail.com>
2009-11-19 21:58:35 -08:00
David Aguilar da680f1231 test: Test the model's 'untracked' attribute
Signed-off-by: David Aguilar <davvid@gmail.com>
2009-11-19 21:58:23 -08:00
David Aguilar c2d466b355 test: Test the model's 'unstaged' attribute
Signed-off-by: David Aguilar <davvid@gmail.com>
2009-11-19 21:58:08 -08:00
David Aguilar 01647f232a test: Add more docstrings to test_cola_models_main
Signed-off-by: David Aguilar <davvid@gmail.com>
2009-11-19 21:57:50 -08:00
David Aguilar 48f93bd93e test: Add a test case for the model's 'modified' attribute
Signed-off-by: David Aguilar <davvid@gmail.com>
2009-11-19 21:51:33 -08:00
David Aguilar d6c423f67c test: Add a test case for the model's 'remote_branches' attribute
Signed-off-by: David Aguilar <davvid@gmail.com>
2009-11-19 21:51:20 -08:00
David Aguilar 1a674917a3 test: Use GitRepositoryTestCase's setup in all tests
This removes redundant repository initialization code in the tests.

Signed-off-by: David Aguilar <davvid@gmail.com>
2009-11-19 21:45:59 -08:00
David Aguilar b0c8e06ff6 test: Simplify and rename helper.TestCase to helper.GitRepositoryTestCase
The test helper module is now simpler.  We no longer import
join, dirname and basename directly.  Several global variables
were removed.

Signed-off-by: David Aguilar <davvid@gmail.com>
2009-11-19 21:22:35 -08:00
David Aguilar 58e8c86553 test: Use local classes in the serialization test suite
There's no reason to have local test classes in the main test helper.

Signed-off-by: David Aguilar <davvid@gmail.com>
2009-11-19 14:39:34 -08:00
David Aguilar 04de42d4f7 models.compare: Derive from ObservableModel
This modifies the CompareModel and BranchCompareModel classes
to not derive from the main model.  They now derive from
observable model instead.  The update_revision_lists() method
is now part of the CompareModel where it is more appropriate.

Signed-off-by: David Aguilar <davvid@gmail.com>
2009-11-19 14:28:46 -08:00
David Aguilar 6ed329a709 views.main: Use gitcmds.log_helper()
Signed-off-by: David Aguilar <davvid@gmail.com>
2009-11-19 14:28:46 -08:00
David Aguilar 0bc7d6bd1e gitcmds: Factor out parse_rev_list() and friends
log_helper(), parse_rev_list(), and rev_list_range()
are now part of the gitcmds module.

Signed-off-by: David Aguilar <davvid@gmail.com>
2009-11-19 14:28:38 -08:00
David Aguilar e6a4610c44 gitcmds: Add log_helper() and parse_rev_list()
Signed-off-by: David Aguilar <davvid@gmail.com>
2009-11-19 14:07:08 -08:00
David Aguilar 1bb2483039 gitcmds: Remove unused upstream_changed_set in worktree_state()
Signed-off-by: David Aguilar <davvid@gmail.com>
2009-11-19 13:58:30 -08:00
David Aguilar 55b453a896 test: Use assertEqual() in the git ops test
Signed-off-by: David Aguilar <davvid@gmail.com>
2009-11-19 13:12:57 -08:00
David Aguilar 57f48dcb64 test: Add unit tests for gitcmds.unstage_paths()
Signed-off-by: David Aguilar <davvid@gmail.com>
2009-11-19 13:12:00 -08:00
David Aguilar 6169bb004d doc: Add git-cola-1.4.1 release notes
Signed-off-by: David Aguilar <davvid@gmail.com>
2009-11-19 12:10:55 -08:00
David Aguilar da1aedd515 doc: Add github and debian issue URLs to release notes
The release notes now link directly to the respective github and
debian issues closed in each release.

Signed-off-by: David Aguilar <davvid@gmail.com>
2009-11-19 12:10:27 -08:00
David Aguilar fff8c628a5 doc: Document cola.models.browser
Signed-off-by: David Aguilar <davvid@gmail.com>
2009-11-19 02:15:34 -08:00
David Aguilar 993eb06c6e repobrowser: Use the BrowserModel
This removes all repobrowser data from the main model.

Signed-off-by: David Aguilar <davvid@gmail.com>
2009-11-19 02:10:48 -08:00
David Aguilar 04e1db4b94 models: Add a BrowserModel for use by the repobrowser
This factors out the data used by the repobrowser.
This should allow us to slim down the main model even further.

Signed-off-by: David Aguilar <davvid@gmail.com>
2009-11-19 02:09:49 -08:00
David Aguilar 00f48f852e commands: Fix a bug in the amend interaction
If we had diffed a staged file then cola wouldn't allow the amend
checkbox to take effect.  This was fixed by restricting the cases
where the Diff command is allowed to change modes.

Signed-off-by: David Aguilar <davvid@gmail.com>
2009-11-19 01:38:38 -08:00
David Aguilar 97e4c09491 gitcmds: Factor out worktree_state() and friends
This finally brings the main model down to a manageable size.
The next step is to break out the browser data which is used
by the repobrowser only.

Signed-off-by: David Aguilar <davvid@gmail.com>
2009-11-19 01:34:16 -08:00
David Aguilar d96358fbff commands: Log format-patch output
Signed-off-by: David Aguilar <davvid@gmail.com>
2009-11-19 01:00:16 -08:00
David Aguilar 62a0ac9aed models.main: Remove unused revert_paths() method
Signed-off-by: David Aguilar <davvid@gmail.com>
2009-11-19 00:53:43 -08:00
David Aguilar 47fc0ac961 views.main: Remove the amend_is_checked() method
Signed-off-by: David Aguilar <davvid@gmail.com>
2009-11-19 00:51:18 -08:00
David Aguilar 05b803b9d6 commands: Fix mistyped 'commit'
Signed-off-by: David Aguilar <davvid@gmail.com>
2009-11-19 00:50:52 -08:00
David Aguilar d31feba27a gitcmds: Factor out format_patchsets()
Signed-off-by: David Aguilar <davvid@gmail.com>
2009-11-19 00:49:43 -08:00
David Aguilar a88d2fcd7c gitcmds: Factory out diff_helper() and commit_diff()
This continues the work of splitting apart the models.main module.

Signed-off-by: David Aguilar <davvid@gmail.com>
2009-11-18 22:07:11 -08:00
David Aguilar 80578687ff cola.diffparse: Prefix utils.write() with the module name
Signed-off-by: David Aguilar <davvid@gmail.com>
2009-11-18 22:01:54 -08:00
David Aguilar 88c11a401d models.main: Remove unused diff_details() method
Signed-off-by: David Aguilar <davvid@gmail.com>
2009-11-18 21:56:29 -08:00
David Aguilar daf633418a models.main: Remove unused add_or_remove() method
Signed-off-by: David Aguilar <davvid@gmail.com>
2009-11-18 21:44:37 -08:00
David Aguilar 1806dbad74 gitcmds: Use the static git command instance
Signed-off-by: David Aguilar <davvid@gmail.com>
2009-11-18 21:38:38 -08:00
David Aguilar dd13071bd3 cola: Add a gitcmd module
The gitcmd module provides access to the static git command object.

Signed-off-by: David Aguilar <davvid@gmail.com>
2009-11-18 21:37:39 -08:00
David Aguilar 659b8d8032 models.main: Trivial whitespace fixup when notifying observers
Signed-off-by: David Aguilar <davvid@gmail.com>
2009-11-18 21:20:47 -08:00
David Aguilar ca528f7b08 gitcmds: Use git-for-each-ref when finding tag names
Signed-off-by: David Aguilar <davvid@gmail.com>
2009-11-18 21:20:22 -08:00
David Aguilar 1c687fb56e gitcmds: Factor out untracked_files()
Signed-off-by: David Aguilar <davvid@gmail.com>
2009-11-18 21:04:58 -08:00
David Aguilar 64533fc958 gitcmds: Factor out tracked_branch()
Signed-off-by: David Aguilar <davvid@gmail.com>
2009-11-18 21:03:52 -08:00
David Aguilar a33a6cc23e gitcmds: Use current_branch() instead of depending on the model
Signed-off-by: David Aguilar <davvid@gmail.com>
2009-11-18 21:03:06 -08:00
David Aguilar 42f9da1f6c gitcmds: Factor out branch_list()
branch_list() no longer lives within the model.
This version also uses git-for-each-ref, which is preferred over
screen-scraping git-branch.

Signed-off-by: David Aguilar <davvid@gmail.com>
2009-11-18 20:59:25 -08:00
David Aguilar cc6be1306c gitcmds: Factor out and optimize current_branch()
This factors current_branch() out of the main model.
This version is faster than the original in that it no longer
calls out to git.  Instead, we read .git/HEAD directly and
cache the result behind a stat() cache.

Signed-off-by: David Aguilar <davvid@gmail.com>
2009-11-18 20:37:46 -08:00
David Aguilar fd1c14c8fd gitcmds: Factor all_files() out from the main model class
Signed-off-by: David Aguilar <davvid@gmail.com>
2009-11-18 20:31:07 -08:00
David Aguilar 95e1de309e gitcmds: Add more docstrings
Signed-off-by: David Aguilar <davvid@gmail.com>
2009-11-18 20:29:46 -08:00
David Aguilar 84e05c1ecd Merge branch 'maint'
* maint:
  views.compare: Add missing button text when comparing commits
  controllers.compare: Fix clashing module and function name
2009-11-18 20:28:22 -08:00
David Aguilar b0a5a9a5a2 views.compare: Add missing button text when comparing commits
Signed-off-by: David Aguilar <davvid@gmail.com>
2009-11-18 20:27:20 -08:00
David Aguilar a71eb88f68 controllers.compare: Fix clashing module and function name
Signed-off-by: David Aguilar <davvid@gmail.com>
2009-11-18 20:26:54 -08:00
David Aguilar 23a15165fa Makefile: Add git-cola.app
Signed-off-by: David Aguilar <davvid@gmail.com>
2009-11-18 19:45:55 -08:00
David Aguilar c2f2d3daaa cola.main: Allow cloning when run outside of a git repository
An open dialog was originally used when cola was run outside of
a git repository.  We now allow cloning a repository as well.

Closes #22

Signed-off-by: David Aguilar <davvid@gmail.com>
2009-11-17 23:25:39 -08:00
David Aguilar 7d28ffb92f cola.views: Add a StartupDialog for use during application startup
We should give the option of either opening or cloning a repo
when cola is run outside of a git repo.  The StartupDialog
provides the GUI interaction for doing so.

Signed-off-by: David Aguilar <davvid@gmail.com>
2009-11-17 23:25:24 -08:00
David Aguilar e552f0c49c cola: Add a guicmds module for clone_repo()
This factors out the GUI logic for cloning from the main view.

Signed-off-by: David Aguilar <davvid@gmail.com>
2009-11-17 23:24:55 -08:00
David Aguilar 95e2280c66 commands: Make spawning optional in the Clone command
We want to be able to initiate a cola session with a clone so
add the ability to clone without spawning a new session.

Signed-off-by: David Aguilar <davvid@gmail.com>
2009-11-17 23:15:11 -08:00
David Aguilar 362586ce16 MANIFEST.in: Add cola/*.py
Signed-off-by: David Aguilar <davvid@gmail.com>
2009-11-17 23:12:09 -08:00
David Aguilar 540b4226f0 Merge branch 'maint-1.4.0'
* maint-1.4.0:
  doc: Add git-cola-1.4.0.5 release notes
  doc: Consolidate release notes onto a single page
  cola.commands: Update status tree when amending commits
  views.main: Ensure that the 'amend' checkbox gets unchecked
  cola.utils: Properly launch external apps on win32

Conflicts:
	share/doc/git-cola/relnotes/releases.rst

Signed-off-by: David Aguilar <davvid@gmail.com>
2009-11-15 21:29:48 -08:00
David Aguilar 21b3b0efd0 doc: Consolidate release notes onto a single page
This makes it simpler to maintain release notes.

Signed-off-by: David Aguilar <davvid@gmail.com>
2009-11-14 10:58:45 -08:00
David Aguilar 211e3ccb86 Merge branch 'maint-1.4.0'
* maint-1.4.0:
  doc: Add git-cola-1.4.0.4 release notes
  Move SSH_ASKPASS scripts to share/git-cola/bin
2009-11-14 04:05:51 -08:00
David Aguilar 8eb1ce6f73 Merge branch 'maint-1.4.0'
* maint-1.4.0:
  doc: Add git-cola-1.4.0.3 release notes
  views.status: Eliminate X11 warnings
2009-11-09 16:04:41 -08:00
David Aguilar 50b82b127e docs: Use 'Git GUI' consistently
There were several places where we were using the phrase 'git gui'
instead of 'Git GUI'.  The latter is preferred.

Signed-off-by: David Aguilar <davvid@gmail.com>
2009-11-05 20:16:53 -08:00
David Aguilar 003e3081d3 Merge branch 'maint-1.4.0' 2009-11-05 18:45:24 -08:00
David Aguilar fd851a6bc9 views.main: Add a dropEvent handler for drag+drop access to git-am
It is now possible to apply patches by dropping them onto cola.
cola also allows you to drop directories in which case cola
walks the directory structure gathering a list of patches, sorts
them alphanumerically, and applies them in order.

Closes #3

Signed-off-by: David Aguilar <davvid@gmail.com>
2009-10-31 15:34:29 -07:00
David Aguilar d52775e800 commands: Add an ApplyPatches command
This will be used to implement drag+drop access to git-am.

Signed-off-by: David Aguilar <davvid@gmail.com>
2009-10-31 15:34:29 -07:00
David Aguilar c1ac1474ed cola.signals: Do not depend on QtCore
The signal names are plain strings now and thus there's no
reason to pass them through QtCore.SIGNAL().  This simplifies
the signals module by making it contain nothing more than a
collection of constants.

Signed-off-by: David Aguilar <davvid@gmail.com>
2009-10-31 15:34:29 -07:00
David Aguilar ad3437453c Makefile: 'test' should depend on 'all'
This ensures that the translation files have been built.

Signed-off-by: David Aguilar <davvid@gmail.com>
2009-10-31 15:34:03 -07:00
301 changed files with 13653 additions and 8461 deletions
+8 -14
View File
@@ -1,27 +1,21 @@
*.deb
*.pyc
*.pyo
*.rpm
*.swp
*~
.DS_Store
.noseids
/git-cola.app
/build
/build-stamp
/ChangeLog
/cola.tuxfamily.org
/cola/builtin_version.py
/config.mak
/configure-stamp
/dist
/debian/*.log
/debian/files
/debian/bld
/debian/cola
/installroot
/.lock-wscript
/Meta
/patches
/releases
/share/locale
/test/tmp
/tags
.DS_Store
.noseids
*.py[co]
*.qm
*.swp
*~
+2 -1
View File
@@ -1 +1,2 @@
Copyright (C) 2009 David Aguilar (davvid@gmail.com) and contributors
Copyright (C) 2007, 2008, 2009, 2010, 2011
David Aguilar <davvid@gmail.com> and contributors
+32 -24
View File
@@ -7,7 +7,7 @@ ACTIVE INGREDIENTS
Python 2.4 or newer
~ http://python.org/
PyQt 4.3 or newer
PyQt 4.1 or newer
~ http://www.riverbankcomputing.co.uk/software/pyqt/download
@@ -27,23 +27,48 @@ If you want to do a global install you can do
# make prefix=/usr install
The "install" target will by default include copies of
simplejson and jsonpickle. If your system already has these
modules installed then you can specify standalone=true when running "make":
$ make prefix=/usr standalone=true install
You don't need to install cola to run it.
git-cola is designed to run directly out of its source tree.
$ make
$ bin/git-cola
$ bin/git-dag
NOW AVAILABLE TO RESIDENTS OF REDMOND, WA
# the only cola made with real sugar, not high fructose corn syrup
MAC OS X INSTALLTION INSTRUCTIONS
Whether you install cola yourself with "make install" or
use the "git-cola.app" bundle, you will need to install
Qt and PyQt separately.
The easiest way to do this is to use brew.
~ http://mxcl.github.com/homebrew/
$ brew install git-cola
Once brew has installed git-cola (and its dependencies) you use
git-cola.app or install from source using "make install".
Installing these packages also gives you a PyQt development
environment which can be used for building your own applications
or hacking on cola itself.
WINDOWS INSTALLATION
Download the latest stable Git, Python 2.6, and Py26-PyQt4 installers
http://code.google.com/p/msysgit/
http://python.org/download/
http://www.riverbankcomputing.co.uk/software/pyqt/download
Download the latest Cola-[version].exe installer
http://cola.tuxfamily.org/releases/win32/
Download the latest git-cola-[version].exe Windows installer
https://github.com/git-cola/git-cola/downloads
Once it's installed you can run it from the Start menu or
from a Git Bash by typing 'cola'.
@@ -55,23 +80,6 @@ tree by using the 'win32/cola' shell script.
$ win32/cola
Note: win32/cola assumes you've installed Python in /c/Python26 or /c/Python25.
Adjust accordingly. You can also add python to your path and simply bin/git-cola.
Adjust accordingly.
ABOUT THE VARIOUS COLA FLAVORS
A cola-*-src tarball does not include any generated files --
it is purely a "source" release and includes everything needed
to generate a cola release. It is basically an export of git-cola.git.
~ http://cola.tuxfamily.org/releases/.
A cola-*-release tarball contains all of the generated gui files and is
designed to be run as-is. These are provided for convenience so that
users are not required to have the PyQt4 development tools installed in
order to use cola.
~ http://cola.tuxfamily.org/releases/release/.
A cola-*-win32 tarball contains extra scripts for use on Windows.
It's easier to just use the .exe installer these days, but if you're
paranoid and would rather just download a tarball then that's cool too.
~ http://cola.tuxfamily.org/releases/win32/.
You can also add python to your path and run bin/git-cola directly.
+14 -16
View File
@@ -1,42 +1,40 @@
include bin/git-cola
include bin/difftool/*
include cola/models/*.py
include bin/git-dag
include cola/*.py
include cola/dag/*.py
include cola/classic/*.py
include cola/controllers/*.py
include cola/main/*.py
include cola/merge/*.py
include cola/models/*.py
include cola/prefs/*.py
include cola/stash/*.py
include cola/views/*.py
include cola/widgets/*.py
include darwin/*
include jsonpickle/LICENSE
include jsonpickle/*.py
include jsonpickle/tests/*.py
include simplejson/LICENSE
include simplejson/*.py
include simplejson/tests/*.py
include test/*.py
include test/Makefile
include README
include ChangeLog
include Makefile
include INSTALL
include LICENSE
include MANIFEST.in
include po/*.po
include share/applications/*.desktop
include share/doc/git-cola/.gitignore
include share/doc/git-cola/*.rst
include share/doc/git-cola/relnotes/*.rst
include share/doc/git-cola/models/*.rst
include share/doc/git-cola/controllers/*.rst
include share/doc/git-cola/views/*.rst
include share/doc/git-cola/images/*.png
include share/doc/git-cola/_build/.gitignore
include share/doc/git-cola/_static/.gitignore
include share/doc/git-cola/_static/*.png
include share/doc/git-cola/_templates/.gitignore
include share/doc/git-cola/*.txt
include share/doc/git-cola/*.conf
include share/doc/git-cola/Makefile
include share/doc/git-cola/Makefile.asciidoc
include share/doc/git-cola/build-docdep.perl
include share/doc/git-cola/callouts.xsl
include share/git-cola/bin/*
include share/git-cola/qm/.gitignore
include share/git-cola/po/*.po
include share/git-cola/icons/*.png
include share/git-cola/styles/*.qss
include share/git-cola/styles/images/*.png
include win32/*
+76 -42
View File
@@ -1,45 +1,66 @@
prefix ?= $(HOME)
DESTDIR ?= /
PYTHON ?= python
PYTHON_VER ?= $(shell $(PYTHON) -c 'import platform; print platform.python_version()[:3]')
PYTHON_SITE ?= $(DESTDIR)$(prefix)/lib/python$(PYTHON_VER)/site-packages
COLA_VER ?= $(shell git describe --abbrev=4 --match='v*.*')
APP ?= git-cola.app
APPZIP ?= $(shell darwin/name-tarball.py)
# The default target of this Makefile is...
all::
all:
# The external commands used by this Makefile are...
GIT = git
NOSETESTS = nosetests
PYTHON = python
TAR = tar
# These values can be overridden on the command-line or via config.mak
prefix = $(HOME)
bindir = $(prefix)/bin
# DESTDIR =
cola_app = $(CURDIR)/git-cola.app
cola_version = $(shell env TERM=dummy $(PYTHON) cola/version.py)
cola_dist := cola-$(cola_version)
python_path = $(CURDIR):$(CURDIR)/thirdparty:$(PYTHONPATH)
python_version = $(shell env TERM=dummy $(PYTHON) -c 'import distutils.sysconfig as sc; print(sc.get_python_version())')
python_site := $(prefix)/lib*/python$(python_version)/site-packages
test_flags =
all_test_flags = --with-doctest $(test_flags)
# User customizations
-include config.mak
ifdef standalone
standalone_args ?= --standalone
endif
all::
$(PYTHON) setup.py build
darwin: all
$(PYTHON) darwin/py2app-setup.py py2app
$(APP): darwin
rm -rf $(APP)
mv dist/$(APP) $(CURDIR)
find $(APP) -name '*_debug*' | xargs rm -f
tar cjf $(APPZIP) $(APP)
install:
$(PYTHON) setup.py install \
--quiet \
install: all
$(PYTHON) setup.py --quiet install \
$(standalone_args) \
--prefix=$(DESTDIR)$(prefix) \
--install-scripts=$(DESTDIR)$(bindir) \
--force && \
rm -f $(PYTHON_SITE)/git_cola* && \
(test -d $(PYTHON_SITE) && rmdir -p $(PYTHON_SITE) 2>/dev/null || true) && \
(cd $(DESTDIR)$(prefix)/bin && \
((! test -e cola && ln -s git-cola cola) || true))
rm -f $(DESTDIR)$(python_site)/git_cola*
rmdir -p $(DESTDIR)$(python_site) 2>/dev/null || true
(cd $(DESTDIR)$(bindir) && \
! test -e cola && ln -s git-cola cola) || true
# Maintainer's dist target
dist:
$(GIT) archive --format=tar --prefix=$(cola_dist)/ HEAD^{tree} | \
gzip -f -9 - >$(cola_dist).tar.gz
doc:
$(MAKE) -C share/doc/git-cola all
$(MAKE) -C share/doc/git-cola prefix=$(prefix) all
html:
$(MAKE) -C share/doc/git-cola html
$(MAKE) -C share/doc/git-cola prefix=$(prefix) html
install-doc:
$(MAKE) -C share/doc/git-cola install
$(MAKE) -C share/doc/git-cola prefix=$(prefix) install
install-html:
$(MAKE) -C share/doc/git-cola install-html
$(MAKE) -C share/doc/git-cola prefix=$(prefix) install-html
uninstall:
rm -rf $(DESTDIR)$(prefix)/bin/git-cola \
@@ -48,26 +69,39 @@ uninstall:
$(DESTDIR)$(prefix)/share/git-cola \
$(DESTDIR)$(prefix)/share/doc/git-cola
test_flags ?=
all_test_flags ?= --with-doctest $(test_flags)
test:
@env PYTHONPATH="$(CURDIR)":"$(PYTHONPATH)" \
nosetests $(all_test_flags)
test: all
@env PYTHONPATH=$(python_path) \
$(NOSETESTS) $(all_test_flags)
coverage:
@env PYTHONPATH=$(CURDIR):$(PYTHONPATH) \
nosetests $(all_test_flags) \
--with-coverage --cover-package=cola
@env PYTHONPATH=$(python_path) \
$(NOSETESTS) --with-coverage --cover-package=cola $(all_test_flags)
clean:
$(MAKE) -C share/doc/git-cola clean
find . -name .noseids -print0 | xargs -0 rm -f
find . -name '*.py[co]' -print0 | xargs -0 rm -f
find share -name '*.qm' -print0 | xargs -0 rm -f
rm -rf cola/builtin_version.* build dist tmp tags
rm -rf build dist tmp tags git-cola.app
rm -rf share/locale
tags:
ctags cola/*.py cola/*/*.py
find . -name '*.py' -print0 | xargs -0 ctags -f tags
.PHONY: all install doc install-doc install-html test clean darwin git-cola.app
pot:
$(PYTHON) setup.py build_pot -N -d .
mo:
$(PYTHON) setup.py build_mo -f
git-cola.app:
mkdir -p $(cola_app)/Contents/MacOS
cp darwin/git-cola $(cola_app)/Contents/MacOS
cp darwin/Info.plist darwin/PkgInfo $(cola_app)/Contents
$(MAKE) prefix=$(cola_app)/Contents/Resources install install-doc
cp darwin/git-cola.icns $(cola_app)/Contents/Resources
app-tarball: git-cola.app
$(TAR) czf git-cola-$(cola_version).app.tar.gz $(cola_app)
.PHONY: all install doc install-doc install-html test clean tags
.PHONY: git-cola.app app-tarball
+25 -34
View File
@@ -1,10 +1,8 @@
git-cola: a highly caffeinated git gui
git-cola: A highly caffeinated git GUI
cola is a sweet, carbonated git gui known for its
sugary flavour and caffeine-inspired features.
git-cola is a simple, powerful, and feature-rich git GUI.
Copyright (C) 2009 David Aguilar and contributors
Copyright (C) 2007-2012, David Aguilar and contributors
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
@@ -19,40 +17,33 @@ GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
REQUIREMENTS
------------
~ git version 1.6.3 or newer
~ Python 2.4 or newer
~ PyQt 4.4 or newer
INSTALLATION
FORK
----
$ git clone git://github.com/git-cola/git-cola.git
$ xdg-open https://github.com/git-cola/git-cola
See the 'INSTALL' file for more details.
DOWNLOAD
--------
~ http://git-cola.github.com/
DISCUSS
-------
~ http://groups.google.com/group/git-cola/
THE WEBSITE
Visit the git-cola home page:
~ http://cola.tuxfamily.org/
THE MAILING LIST
Visit us on google groups:
~ http://groups.google.com/group/git-cola/
Feel free to join, suggest features, and submit patches with
git-send-mail.
THE BUG TRACKER
We use the github issue tracker. It's simple and it works.
~ http://github.com/davvid/git-cola/issues
We do prefer to discuss things on the mailing list, though,
so don't be shy.
Join, suggest features, and send patches with 'git send-mail'.
SUPPORT
-------
~ http://github.com/git-cola/git-cola/issues
ACKNOWLEDGEMENTS
git-cola includes a copy of the following modules:
GitPython ~ http://gitorious.org/projects/git-python/
jsonpickle ~ http://code.google.com/p/jsonpickle/
----------------
git-cola includes the following modules:
jsonpickle ~ https://github.com/jsonpickle/jsonpickle
simplejson ~ http://code.google.com/p/simplejson/
-8
View File
@@ -1,8 +0,0 @@
git-difftool originated in git-cola.
It first appeared in git.git's contrib/ area in January 2009
after it was rewritten as a GIT_EXTERNAL_DIFF compatible script.
git-difftool became an official git command in April 2009 with the
release of git 1.6.3. setup.py checks for git 1.6.3 and installs
git-difftool when an older git version is detected.
-85
View File
@@ -1,85 +0,0 @@
#!/usr/bin/env perl
# Copyright (c) 2009 David Aguilar
#
# This is a wrapper around the GIT_EXTERNAL_DIFF-compatible
# git-difftool--helper script.
#
# This script exports GIT_EXTERNAL_DIFF and GIT_PAGER for use by git.
# GIT_DIFFTOOL_NO_PROMPT, GIT_DIFFTOOL_PROMPT, and GIT_DIFF_TOOL
# are exported for use by git-difftool--helper.
#
# Any arguments that are unknown to this script are forwarded to 'git diff'.
use strict;
use warnings;
use Cwd qw(abs_path);
use File::Basename qw(dirname);
my $DIR = abs_path(dirname($0));
sub usage
{
print << 'USAGE';
usage: git difftool [--tool=<tool>] [-y|--no-prompt] ["git diff" options]
USAGE
exit 1;
}
sub setup_environment
{
$ENV{PATH} = "$DIR:$ENV{PATH}";
$ENV{GIT_PAGER} = '';
$ENV{GIT_EXTERNAL_DIFF} = 'git-difftool--helper';
}
sub exe
{
my $exe = shift;
if ($^O eq 'MSWin32' || $^O eq 'msys') {
return "$exe.exe";
}
return $exe;
}
sub generate_command
{
my @command = (exe('git'), 'diff');
my $skip_next = 0;
my $idx = -1;
for my $arg (@ARGV) {
$idx++;
if ($skip_next) {
$skip_next = 0;
next;
}
if ($arg eq '-t' || $arg eq '--tool') {
usage() if $#ARGV <= $idx;
$ENV{GIT_DIFF_TOOL} = $ARGV[$idx + 1];
$skip_next = 1;
next;
}
if ($arg =~ /^--tool=/) {
$ENV{GIT_DIFF_TOOL} = substr($arg, 7);
next;
}
if ($arg eq '-y' || $arg eq '--no-prompt') {
$ENV{GIT_DIFFTOOL_NO_PROMPT} = 'true';
delete $ENV{GIT_DIFFTOOL_PROMPT};
next;
}
if ($arg eq '--prompt') {
$ENV{GIT_DIFFTOOL_PROMPT} = 'true';
delete $ENV{GIT_DIFFTOOL_NO_PROMPT};
next;
}
if ($arg eq '-h' || $arg eq '--help') {
usage();
}
push @command, $arg;
}
return @command
}
setup_environment();
exec(generate_command());
-227
View File
@@ -1,227 +0,0 @@
#!/bin/sh
# git-difftool--helper is a GIT_EXTERNAL_DIFF-compatible diff tool launcher.
# This script is typically launched by using the 'git difftool'
# convenience command.
#
# Copyright (c) 2009 David Aguilar
# difftool.prompt controls the default prompt/no-prompt behavior
# and is overridden with $GIT_DIFFTOOL*_PROMPT.
should_prompt () {
prompt=$(git config --bool difftool.prompt || echo true)
if test "$prompt" = true; then
test -z "$GIT_DIFFTOOL_NO_PROMPT"
else
test -n "$GIT_DIFFTOOL_PROMPT"
fi
}
# This function prepares temporary files and launches the appropriate
# merge tool.
launch_merge_tool () {
# Merged is the filename as it appears in the work tree
# Local is the contents of a/filename
# Remote is the contents of b/filename
# Custom merge tool commands might use $BASE so we provide it
MERGED="$1"
LOCAL="$2"
REMOTE="$3"
BASE="$1"
# $LOCAL and $REMOTE are temporary files so prompt
# the user with the real $MERGED name before launching $merge_tool.
if should_prompt; then
printf "\nViewing: '$MERGED'\n"
printf "Hit return to launch '%s': " "$merge_tool"
read ans
fi
# Run the appropriate merge tool command
case "$merge_tool" in
kdiff3)
basename=$(basename "$MERGED")
"$merge_tool_path" --auto \
--L1 "$basename (A)" \
--L2 "$basename (B)" \
"$LOCAL" "$REMOTE" \
> /dev/null 2>&1
;;
kompare)
"$merge_tool_path" "$LOCAL" "$REMOTE"
;;
tkdiff)
"$merge_tool_path" "$LOCAL" "$REMOTE"
;;
meld)
"$merge_tool_path" "$LOCAL" "$REMOTE"
;;
diffuse)
"$merge_tool_path" "$LOCAL" "$REMOTE" | cat
;;
vimdiff)
"$merge_tool_path" -d -c "wincmd l" "$LOCAL" "$REMOTE"
;;
gvimdiff)
"$merge_tool_path" -d -c "wincmd l" -f "$LOCAL" "$REMOTE"
;;
xxdiff)
"$merge_tool_path" \
-R 'Accel.Search: "Ctrl+F"' \
-R 'Accel.SearchForward: "Ctrl-G"' \
"$LOCAL" "$REMOTE"
;;
opendiff)
"$merge_tool_path" "$LOCAL" "$REMOTE" | cat
;;
ecmerge)
"$merge_tool_path" "$LOCAL" "$REMOTE" \
--default --mode=merge2 --to="$MERGED"
;;
emerge)
"$merge_tool_path" -f emerge-files-command \
"$LOCAL" "$REMOTE" "$(basename "$MERGED")"
;;
*)
if test -n "$merge_tool_cmd"; then
( eval $merge_tool_cmd )
fi
;;
esac
}
# Verifies that (difftool|mergetool).<tool>.cmd exists
valid_custom_tool() {
merge_tool_cmd="$(git config difftool.$1.cmd)"
test -z "$merge_tool_cmd" &&
merge_tool_cmd="$(git config mergetool.$1.cmd)"
test -n "$merge_tool_cmd"
}
# Verifies that the chosen merge tool is properly setup.
# Built-in merge tools are always valid.
valid_tool() {
case "$1" in
kdiff3 | kompare | tkdiff | xxdiff | meld | opendiff | emerge | vimdiff | gvimdiff | ecmerge)
;; # happy
*)
if ! valid_custom_tool "$1"
then
return 1
fi
;;
esac
}
# Sets up the merge_tool_path variable.
# This handles the difftool.<tool>.path configuration.
# This also falls back to mergetool defaults.
init_merge_tool_path() {
merge_tool_path=$(git config difftool."$1".path)
test -z "$merge_tool_path" &&
merge_tool_path=$(git config mergetool."$1".path)
if test -z "$merge_tool_path"; then
case "$1" in
vimdiff)
merge_tool_path=vim
;;
gvimdiff)
merge_tool_path=gvim
;;
emerge)
merge_tool_path=emacs
;;
*)
merge_tool_path="$1"
;;
esac
fi
}
# Allow GIT_DIFF_TOOL and GIT_MERGE_TOOL to provide default values
test -n "$GIT_MERGE_TOOL" && merge_tool="$GIT_MERGE_TOOL"
test -n "$GIT_DIFF_TOOL" && merge_tool="$GIT_DIFF_TOOL"
# If merge tool was not specified then use the diff.tool
# configuration variable. If that's invalid then reset merge_tool.
# Fallback to merge.tool.
if test -z "$merge_tool"; then
merge_tool=$(git config diff.tool)
test -z "$merge_tool" &&
merge_tool=$(git config merge.tool)
if test -n "$merge_tool" && ! valid_tool "$merge_tool"; then
echo >&2 "git config option diff.tool set to unknown tool: $merge_tool"
echo >&2 "Resetting to default..."
unset merge_tool
fi
fi
# Try to guess an appropriate merge tool if no tool has been set.
if test -z "$merge_tool"; then
# We have a $DISPLAY so try some common UNIX merge tools
if test -n "$DISPLAY"; then
# If gnome then prefer meld, otherwise, prefer kdiff3 or kompare
if test -n "$GNOME_DESKTOP_SESSION_ID" ; then
merge_tool_candidates="meld kdiff3 kompare tkdiff xxdiff gvimdiff diffuse"
else
merge_tool_candidates="kdiff3 kompare tkdiff xxdiff meld gvimdiff diffuse"
fi
fi
if echo "${VISUAL:-$EDITOR}" | grep 'emacs' > /dev/null 2>&1; then
# $EDITOR is emacs so add emerge as a candidate
merge_tool_candidates="$merge_tool_candidates emerge opendiff vimdiff"
elif echo "${VISUAL:-$EDITOR}" | grep 'vim' > /dev/null 2>&1; then
# $EDITOR is vim so add vimdiff as a candidate
merge_tool_candidates="$merge_tool_candidates vimdiff opendiff emerge"
else
merge_tool_candidates="$merge_tool_candidates opendiff emerge vimdiff"
fi
echo "merge tool candidates: $merge_tool_candidates"
# Loop over each candidate and stop when a valid merge tool is found.
for i in $merge_tool_candidates
do
init_merge_tool_path $i
if type "$merge_tool_path" > /dev/null 2>&1; then
merge_tool=$i
break
fi
done
if test -z "$merge_tool" ; then
echo "No known merge resolution program available."
exit 1
fi
else
# A merge tool has been set, so verify that it's valid.
if ! valid_tool "$merge_tool"; then
echo >&2 "Unknown merge tool $merge_tool"
exit 1
fi
init_merge_tool_path "$merge_tool"
if test -z "$merge_tool_cmd" && ! type "$merge_tool_path" > /dev/null 2>&1; then
echo "The merge tool $merge_tool is not available as '$merge_tool_path'"
exit 1
fi
fi
# Launch the merge tool on each path provided by 'git diff'
while test $# -gt 6
do
launch_merge_tool "$1" "$2" "$5"
shift 7
done
+15 -24
View File
@@ -4,7 +4,8 @@
"""
__copyright__ = """
Copyright (C) 2007, 2008, 2009 David Aguilar <davvid@gmail.com> and contributors
Copyright (C) 2007, 2008, 2009, 2010,
David Aguilar <davvid@gmail.com> and contributors
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License version 2 as
@@ -15,9 +16,6 @@ but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software
Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
"""
import os
@@ -27,28 +25,21 @@ def cola_init():
"""Provides access to the cola modules"""
# Try to detect where it is run from and set prefix and the search path.
# It is assumed that the user installed Cola using the --prefix= option
prefix = os.path.dirname(os.path.abspath(__file__))
if 'Contents/Resources' not in __file__:
prefix = os.path.dirname(prefix)
# Look for modules in the source and install trees
paths_to_try = [prefix, os.path.join(prefix, 'share', 'git-cola', 'lib')]
actual_paths = [p for p in paths_to_try if os.path.exists(p)]
sys.path = actual_paths + sys.path
import cola.resources
# Setup the path so that git finds us when we run 'git cola'
path_entries = os.environ.get('PATH').split(os.pathsep)
bindir = os.path.dirname(os.path.abspath(__file__))
path_entries.insert(0, bindir)
path = os.pathsep.join(path_entries)
os.environ['PATH'] = path
os.putenv('PATH', path)
prefix = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
# Look for modules in the source or install trees
thirdparty = os.path.join(prefix, 'thirdparty')
if os.path.exists(thirdparty):
# Source tree: prefer cola's modules but allow system versions of thirdparty
sys.path.insert(0, prefix)
sys.path.append(thirdparty)
else:
# Install tree: prefer cola
paths_to_try = [os.path.join(prefix, 'share', 'git-cola', 'lib')]
sys.path = paths_to_try + sys.path
if __name__ == '__main__':
# lights, cameras, action
cola_init()
import cola.main
cola.main.main()
import cola.app
cola.app.main('git-cola')
Executable
+46
View File
@@ -0,0 +1,46 @@
#!/usr/bin/env python
# -*- python-mode -*-
"""git-dag: An advanced git DAG visualizer
"""
__copyright__ = """
Copyright (C) 2007, 2008, 2009, 2010, 2011, 2012
David Aguilar <davvid@gmail.com> and contributors
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License version 2 as
published by the Free Software Foundation.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
"""
import os
import sys
def cola_init():
"""Provides access to the cola modules"""
# Try to detect where it is run from and set prefix and the search path.
# It is assumed that the user installed Cola using the --prefix= option
prefix = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
# Look for modules in the source or install trees
thirdparty = os.path.join(prefix, 'thirdparty')
if os.path.exists(thirdparty):
# Source tree: prefer cola's modules but allow system versions of thirdparty
sys.path.insert(0, prefix)
sys.path.append(thirdparty)
else:
# Install tree: prefer cola
paths_to_try = [os.path.join(prefix, 'share', 'git-cola', 'lib')]
sys.path = paths_to_try + sys.path
if __name__ == '__main__':
# lights, cameras, action
cola_init()
import cola.app
cola.app.main('git-dag')
+1 -1
View File
@@ -1,5 +1,5 @@
from cola.cmdfactory import factory
from cola.models.main import model
from cola.main.model import model
from cola.models.selection import selection
from cola.models.selection import selection_model
from cola.models.selection import single_selection
+274 -23
View File
@@ -1,15 +1,50 @@
# Copyright (C) 2009, David Aguilar <davvid@gmail.com>
"""Provides the cola QApplication subclass"""
# style note: we use camelCase here since we're masquerading a Qt class
"""Provides the main() routine and ColaApplicaiton"""
import glob
import optparse
import os
import signal
import sys
from PyQt4 import QtCore
from PyQt4 import QtGui
from cola import utils
from cola import git
from cola import i18n
from cola import inotify
from cola import resources
from cola import signals
from cola import version
from cola.decorators import memoize
def setup_environment():
# Spoof an X11 display for SSH
os.environ.setdefault('DISPLAY', ':0')
# Provide an SSH_ASKPASS fallback
if sys.platform == 'darwin':
os.environ.setdefault('SSH_ASKPASS',
resources.share('bin', 'ssh-askpass-darwin'))
else:
os.environ.setdefault('SSH_ASKPASS',
resources.share('bin', 'ssh-askpass'))
# Setup the path so that git finds us when we run 'git cola'
path_entries = os.environ.get('PATH').split(os.pathsep)
bindir = os.path.dirname(os.path.abspath(__file__))
path_entries.insert(0, bindir)
path = os.pathsep.join(path_entries)
os.environ['PATH'] = path
os.putenv('PATH', path)
@memoize
def instance(argv):
from PyQt4 import QtGui
return QtGui.QApplication(list(argv))
# style note: we use camelCase here since we're masquerading a Qt class
class ColaApplication(object):
"""The main cola application
@@ -19,9 +54,17 @@ class ColaApplication(object):
def __init__(self, argv, locale=None, gui=True):
"""Initialize our QApplication for translation
"""
# monkey-patch Qt's app translate() to handle .po files
from PyQt4 import QtGui
from PyQt4 import QtCore
from cola import cmds
from cola import utils
i18n.install(locale)
# monkey-patch Qt's translate() to use our translate()
if gui:
self._app = QtGui.QApplication(argv)
self._app = instance(tuple(argv))
self._app.setWindowIcon(QtGui.QIcon(resources.icon('git.svg')))
self._translate_base = QtGui.QApplication.translate
QtGui.QApplication.translate = self.translate
@@ -30,21 +73,22 @@ class ColaApplication(object):
self._translate_base = QtCore.QCoreApplication.translate
QtCore.QCoreApplication.translate = self.translate
# Find the current language settings and apply them
if not locale:
locale = str(QtCore.QLocale().system().name())
# Register model commands
cmds.register()
qmfile = utils.qm_for_locale(locale)
if os.path.exists(qmfile):
translator = QtCore.QTranslator(self._app)
translator.load(qmfile)
self._app.installTranslator(translator)
# Make file descriptors binary for win32
utils.set_binary(sys.stdin)
utils.set_binary(sys.stdout)
utils.set_binary(sys.stderr)
def translate(self, context, *args):
"""Supports @@noun/@@verb and context-less translation
def translate(self, domain, txt):
"""
# We set the context to '' to properly handle .qm files
trtxt = unicode(self._translate_base('', *args))
Translate strings with gettext
Supports @@noun/@@verb specifiers.
"""
trtxt = i18n.gettext(txt)
if trtxt[-6:-4] == '@@': # handle @@verb / @@noun
trtxt = trtxt[:-6]
return trtxt
@@ -57,6 +101,213 @@ class ColaApplication(object):
"""Wrap exec_()"""
return self._app.exec_()
def setStyleSheet(self, txt):
"""Wrap setStyleSheet(txt)"""
return self._app.setStyleSheet(txt)
def parse_args(context):
parser = optparse.OptionParser(usage='%prog [options]')
# We also accept 'git cola version'
parser.add_option('-v', '--version',
help='Show cola version',
dest='version',
default=False,
action='store_true')
# Accept git cola --classic
parser.add_option('--classic',
help='Launch cola classic',
dest='classic',
default=False,
action='store_true')
# Specifies a git repository to open
parser.add_option('-r', '--repo',
help='Specifies the path to a git repository.',
dest='repo',
metavar='PATH',
default=os.getcwd())
# Specifies that we should prompt for a repository at startup
parser.add_option('--prompt',
help='Prompt for a repository before starting the main GUI.',
dest='prompt',
action='store_true',
default=False)
# Used on Windows for adding 'git' to the path
parser.add_option('-g', '--git-path',
help='Specifies the path to the git binary',
dest='git',
metavar='PATH',
default='')
if context == 'git-dag':
parser.add_option('-c', '--count',
help='Number of commits to display.',
dest='count',
type='int',
default=1000)
return parser.parse_args()
def process_args(opts, args):
if opts.version or (args and args[0] == 'version'):
# Accept 'git cola --version' or 'git cola version'
print 'cola version', version.version()
sys.exit(0)
if opts.git:
# Adds git to the PATH. This is needed on Windows.
path_entries = os.environ.get('PATH', '').split(os.pathsep)
path_entries.insert(0, os.path.dirname(opts.git))
os.environ['PATH'] = os.pathsep.join(path_entries)
# Bail out if --repo is not a directory
repo = os.path.realpath(opts.repo)
if not os.path.isdir(repo):
print >> sys.stderr, "fatal: '%s' is not a directory. Consider supplying -r <path>.\n" % repo
sys.exit(-1)
# We do everything relative to the repo root
os.chdir(opts.repo)
return repo
def main(context):
"""Parses the command-line arguments and starts git-cola
"""
setup_environment()
opts, args = parse_args(context)
repo = process_args(opts, args)
try:
# Defer these imports to allow git cola --version without pyqt installed
from PyQt4 import QtCore
except ImportError:
print >> sys.stderr, 'Sorry, you do not seem to have PyQt4 installed.'
print >> sys.stderr, 'Please install it before using cola.'
print >> sys.stderr, 'e.g.: sudo apt-get install python-qt4'
sys.exit(-1)
from PyQt4.QtCore import SIGNAL
# Import cola modules
import cola
from cola import qtcompat
qtcompat.install()
from cola import guicmds
from cola.app import ColaApplication
from cola.classic import cola_classic
from cola.dag import git_dag
from cola.widgets import cfgactions
from cola.widgets import startup
from cola.main.view import MainView
from cola.main.controller import MainController
# Allow Ctrl-C to exit
signal.signal(signal.SIGINT, signal.SIG_DFL)
# Add the default style dir so that we find our icons
_setup_resource_dir(resources.icon_dir())
# Initialize the app
app = ColaApplication(sys.argv)
# Ensure that we're working in a valid git repository.
# If not, try to find one. When found, chdir there.
model = cola.model()
valid = model.use_worktree(repo) and not opts.prompt
while not valid:
startup_dlg = startup.StartupDialog(app.activeWindow())
gitdir = startup_dlg.find_git_repo()
if not gitdir:
sys.exit(-1)
valid = model.use_worktree(gitdir)
# Finally, go to the root of the git repo
os.chdir(model.git.worktree())
# Show the GUI
if opts.classic:
view = cola_classic(update=False)
elif context == 'git-cola':
view = MainView(model)
ctl = MainController(model, view)
elif context == 'git-dag':
ctl = git_dag(model, app.activeWindow(), opts=opts, args=args)
view = ctl.view
# Install UI wrappers for command objects
cfgactions.install_command_wrapper(view)
guicmds.install_command_wrapper(view)
# Show the view and start the main event loop
view.show()
# Make sure that we start out on top
view.raise_()
# Scan for the first time
task = _start_update_thread(model)
# Start the inotify thread
inotify.start()
msg_timer = QtCore.QTimer()
msg_timer.setSingleShot(True)
msg_timer.connect(msg_timer, SIGNAL('timeout()'), _send_msg)
msg_timer.start(0)
# Start the event loop
result = app.exec_()
# All done, cleanup
inotify.stop()
QtCore.QThreadPool.globalInstance().waitForDone()
pattern = cola.model().tmp_file_pattern()
for filename in glob.glob(pattern):
os.unlink(filename)
sys.exit(result)
return ctl, task
def _start_update_thread(model):
"""Update the model in the background
git-cola should startup as quickly as possible.
"""
from PyQt4 import QtCore
class UpdateTask(QtCore.QRunnable):
def run(self):
model.update_status(update_index=True)
# Hold onto a reference to prevent PyQt from dereferencing
task = UpdateTask()
QtCore.QThreadPool.globalInstance().start(task)
return task
def _send_msg():
import cola
git.GIT_COLA_TRACE = os.getenv('GIT_COLA_TRACE', False)
if git.GIT_COLA_TRACE:
msg = ('info: Trace enabled. '
'Many of commands reported with "trace" use git\'s stable '
'"plumbing" API and are not intended for typical '
'day-to-day use. Here be dragons')
cola.notifier().broadcast(signals.log_cmd, 0, msg)
def _setup_resource_dir(dirname):
"""Adds resource directories to Qt's search path"""
from cola import qtcompat
basename = os.path.basename(dirname)
qtcompat.add_search_path(basename, dirname)
+135
View File
@@ -0,0 +1,135 @@
# Copyright (c) 2009 David Aguilar
"""Provides a serializable data container"""
import types
import copy
from cola.compat import set
class BaseModel(object):
def param_names(self, export=False):
"""Returns a list of serializable attribute names.
>>> m = BaseModel()
>>> m._question = 'unknown'
>>> m.answer = 42
>>> m.param_names()
['answer']
>>> m.param_names(export=True)
['_question', 'answer']
"""
names = []
for k, v in self.__dict__.iteritems():
if is_atom(v) or is_dict(v) or is_seq(v):
if k.strip('_') != k:
if not export:
continue
names.append(k)
names.sort()
return names
def has_param(self,param):
"""Returns true if a parameter exists in a model.
>>> m = BaseModel()
>>> m.answer = 42
>>> m.has_param('answer')
True
>>> m.has_param('question')
False
"""
return param in self.__dict__
def param(self, param, default=None):
"""Returns the value of a model parameter.
>>> m = BaseModel()
>>> m.answer = 42
>>> m.param('answer')
42
>>> m.param('another answer', 42)
42
"""
return self.__dict__.get(param, default)
def __getattr__(self, param):
"""Provides set_<attribute>(value) append methods.
This provides automatic convenience methods for handling
setattrs with notification.
>>> m = BaseModel()
>>> m.answer = 42
>>> m.set_answer(41)
>>> # Observers are notified
>>> m.answer
41
"""
# Base case: we actually have this param
if param in self.__dict__:
return getattr(self, param)
# Return a closure over param for calling set_param;
# Concrete classes subclass set_param to provide notification
if param.startswith('set_'):
return lambda v: self.set_param(param[4:], v)
errmsg = ("'%s' object has no attribute '%s'" %
(self.__class__.__name__, param))
raise AttributeError(errmsg)
def set_param(self, param, value):
"""Wrapper around setattr()
>>> m = BaseModel()
>>> m.answer = 41
>>> m.set_param('answer', 42)
>>> m.answer
42
"""
setattr(self, param, value)
def copy_params(self, model, params_to_copy=None):
"""Copies params from one model to another.
>>> m = BaseModel()
>>> m.answer = 42
>>> m._question = 'unknown'
>>> m2 = BaseModel()
>>> m2.copy_params(m)
>>> m2._question
'unknown'
>>> m2.answer
42
"""
# Loop over all attributes and copy them over
for k in params_to_copy or model.param_names(export=True):
self.__dict__[k] = copy.copy(model.__dict__[k])
def is_dict(item):
return type(item) is types.DictType
def is_seq(item):
return (type(item) is types.ListType or
type(item) is types.TupleType or
type(item) is set)
def is_atom(item):
return(item is None or
type(item) in types.StringTypes or
type(item) is types.BooleanType or
type(item) is types.IntType or
type(item) is types.LongType or
type(item) is types.FloatType or
type(item) is types.ComplexType)
+2
View File
@@ -0,0 +1,2 @@
from cola.classic.controller import cola_classic
from cola.classic.controller import widget as classic_widget
@@ -1,5 +1,4 @@
import os
from PyQt4 import QtGui
from PyQt4 import QtCore
from PyQt4.QtCore import SIGNAL
@@ -7,25 +6,31 @@ import cola
import cola.utils
import cola.difftool
from cola import gitcmds
from cola import qtutils
from cola import signals
from cola.controllers.selectcommits import select_commits
from cola.models import gitrepo
from cola.views import repo
from cola.classic.view import RepoDialog
from cola.classic.model import GitRepoModel
from cola.classic.model import GitRepoEntryManager
from cola.compat import set
def widget():
def widget(parent=None, update=True):
"""Return a widget for immediate use."""
view = repo.RepoTreeView()
view.setModel(gitrepo.GitRepoModel(view))
controller = ClassicController(view)
view = RepoDialog(parent=parent, update=update)
view.tree.setModel(GitRepoModel(view.tree))
controller = ClassicController(view.tree)
return view
def cola_classic():
def cola_classic(parent=None, update=True):
"""Launch a new cola classic session."""
view = widget()
view.raise_()
view = widget(parent=parent, update=update)
if not parent:
qtutils.center_on_screen(view)
view.show()
return view
class ClassicController(QtCore.QObject):
@@ -53,15 +58,16 @@ class ClassicController(QtCore.QObject):
if path in self.updated:
return
self.updated.add(path)
gitrepo.GitRepoEntryManager.entry(path).update()
GitRepoEntryManager.entry(path).update()
entry = GitRepoEntryManager.entry
for row in xrange(item.rowCount()):
path = item.child(row, 0).path
gitrepo.GitRepoEntryManager.entry(path).update()
entry(path).update()
def difftool_predecessor(self, paths):
"""Prompt for an older commit and launch difftool against it."""
args = ['--'] + paths
revs, summaries = self.model.log_helper(all=True, extra_args=args)
revs, summaries = gitcmds.log_helper(all=True, extra_args=args)
commits = select_commits('Select Previous Version',
revs, summaries, multiselect=False)
if not commits:
@@ -72,7 +78,6 @@ class ClassicController(QtCore.QObject):
if __name__ == '__main__':
import sys
from PyQt4 import QtGui
app = QtGui.QApplication(sys.argv)
cola_classic()
+105 -32
View File
@@ -1,5 +1,4 @@
import os
import sys
import time
from PyQt4 import QtCore
@@ -8,9 +7,14 @@ from PyQt4.QtCore import Qt
from PyQt4.QtCore import SIGNAL
import cola
from cola import gitcfg
from cola import core
from cola import utils
from cola import qtutils
from cola import version
from cola import resources
from cola.compat import set
# Custom event type for GitRepoInfoEvents
INFO_EVENT_TYPE = QtCore.QEvent.User + 42
@@ -29,9 +33,11 @@ class Columns(object):
class GitRepoModel(QtGui.QStandardItemModel):
"""Provides an interface into a git repository for browsing purposes."""
def __init__(self, parent):
QtGui.QStandardItem.__init__(self, parent)
QtGui.QStandardItemModel.__init__(self, parent)
self._interesting_paths = self._get_paths()
self._known_paths = set()
self.connect(self, SIGNAL('updated'), self._updated_callback)
model = cola.model()
model.add_message_observer(model.message_updated,
self._model_updated)
@@ -120,6 +126,9 @@ class GitRepoModel(QtGui.QStandardItemModel):
def _model_updated(self):
"""Observes model changes and updates paths accordingly."""
self.emit(SIGNAL('updated'))
def _updated_callback(self):
old_paths = self._interesting_paths
new_paths = self._get_paths()
for path in new_paths.union(old_paths):
@@ -154,15 +163,16 @@ class GitRepoModel(QtGui.QStandardItemModel):
entries = dirname.split('/')
curdir = []
parent = self.invisibleRootItem()
curdir_append = curdir.append
self_add_directory = self.add_directory
for entry in entries:
curdir.append(entry)
curdir_append(entry)
path = '/'.join(curdir)
if path in direntries:
parent = direntries[path]
else:
grandparent = parent
parent_path = '/'.join(curdir[:-1])
parent = self.add_directory(grandparent, path)
parent = self_add_directory(grandparent, path)
direntries[path] = parent
return parent
@@ -178,11 +188,49 @@ class GitRepoEntryManager(object):
static_entries = {}
@classmethod
def entry(cls, path):
def entry(cls, path, _static_entries=static_entries):
"""Return a static instance of a GitRepoEntry."""
if path not in cls.static_entries:
cls.static_entries[path] = GitRepoEntry(path)
return cls.static_entries[path]
try:
e = _static_entries[path]
except KeyError:
e = _static_entries[path] = GitRepoEntry(path)
return e
class TaskRunner(object):
"""Manages QRunnable task instances to avoid python's garbage collector
When PyQt stops referencing a QRunnable instance Python cleans it up
which leads to segfaults, e.g. the dreaded "C++ object has gone away".
This class keeps track of tasks and cleans up references to them as they
complete.
"""
singleton = None
@classmethod
def instance(cls):
if cls.singleton is None:
cls.singleton = TaskRunner()
return cls.singleton
def __init__(self):
self.tasks = set()
self.threadpool = QtCore.QThreadPool.globalInstance()
self.notifier = QtCore.QObject()
self.notifier.connect(self.notifier, SIGNAL('task_done'), self.task_done)
def run(self, task):
self.tasks.add(task)
self.threadpool.start(task)
def task_done(self, task):
if task in self.tasks:
self.tasks.remove(task)
def cleanup_task(self, task):
self.notifier.emit(SIGNAL('task_done'), task)
class GitRepoEntry(QtCore.QObject):
@@ -195,7 +243,6 @@ class GitRepoEntry(QtCore.QObject):
def __init__(self, path):
QtCore.QObject.__init__(self)
self.path = path
self.task = None
def update_name(self):
"""Emits a signal corresponding to the entry's name."""
@@ -207,9 +254,8 @@ class GitRepoEntry(QtCore.QObject):
def update(self):
"""Starts a GitRepoInfoTask to calculate info for entries."""
# GitRepoInfoTask handles expensive lookups
threadpool = QtCore.QThreadPool.globalInstance()
self.task = GitRepoInfoTask(self.path)
threadpool.start(self.task)
task = GitRepoInfoTask(self.path)
TaskRunner.instance().run(task)
def event(self, e):
"""Receive GitRepoInfoEvents and emit corresponding Qt signals."""
@@ -220,11 +266,19 @@ class GitRepoEntry(QtCore.QObject):
return QtCore.QObject.event(self, e)
class GitRepoInfoTask(QtCore.QRunnable):
# Support older versions of PyQt
if version.check('pyqt_qrunnable', QtCore.PYQT_VERSION_STR):
QRunnable = QtCore.QRunnable
else:
class QRunnable(object):
pass
class GitRepoInfoTask(QRunnable):
"""Handles expensive git lookups for a path."""
def __init__(self, path):
QtCore.QRunnable.__init__(self)
QRunnable.__init__(self)
self.path = path
self._cfg = gitcfg.instance()
self._data = {}
def data(self, key):
@@ -238,18 +292,18 @@ class GitRepoInfoTask(QtCore.QRunnable):
log_line = cola.model().git.log('-1', '--', self.path,
M=True,
all=True,
pretty='format:%ar/%s/%an')
no_color=True,
pretty='format:%ar%x01%s%x01%an')
if log_line:
log_line = core.decode(log_line)
date, rest = log_line.split('/', 1)
message, author = rest.rsplit('/', 1)
date, message, author = log_line.split(chr(0x01), 2)
self._data['date'] = date
self._data['message'] = message
self._data['author'] = author
else:
self._data['date'] = self.date()
self._data['message'] = '-'
self._data['author'] = cola.model().local_user_name
self._data['author'] = self._cfg.get('user.name', 'unknown')
return self._data[key]
def name(self):
@@ -286,31 +340,39 @@ class GitRepoInfoTask(QtCore.QRunnable):
upstream_changed = utils.add_parents(set(model.upstream_changed))
if self.path in unmerged:
return qtutils.tr('Unmerged')
return (resources.icon('sigil-unmerged.png'),
qtutils.tr('Unmerged'))
if self.path in modified and self.path in staged:
return qtutils.tr('Partially Staged')
return (resources.icon('sigil-partial.png'),
qtutils.tr('Partially Staged'))
if self.path in modified:
return qtutils.tr('Modified')
return (resources.icon('sigil-modified.png'),
qtutils.tr('Modified'))
if self.path in staged:
return qtutils.tr('Staged')
if self.path in untracked:
return qtutils.tr('Untracked')
return (resources.icon('sigil-staged.png'),
qtutils.tr('Staged'))
if self.path in upstream_changed:
return qtutils.tr('Changed Upstream')
return '-'
return (resources.icon('sigil-upstream.png'),
qtutils.tr('Changed Upstream'))
if self.path in untracked:
return (None, '?')
return (None, '')
def run(self):
"""Perform expensive lookups and post corresponding events."""
app = QtGui.QApplication.instance()
app.postEvent(GitRepoEntryManager.entry(self.path),
entry = GitRepoEntryManager.entry(self.path)
app.postEvent(entry,
GitRepoInfoEvent(Columns.MESSAGE, self.data('message')))
app.postEvent(GitRepoEntryManager.entry(self.path),
app.postEvent(entry,
GitRepoInfoEvent(Columns.AGE, self.data('date')))
app.postEvent(GitRepoEntryManager.entry(self.path),
app.postEvent(entry,
GitRepoInfoEvent(Columns.WHO, self.data('author')))
app.postEvent(GitRepoEntryManager.entry(self.path),
app.postEvent(entry,
GitRepoInfoEvent(Columns.STATUS, self.status()))
TaskRunner.instance().cleanup_task(self)
class GitRepoInfoEvent(QtCore.QEvent):
"""Transport mechanism for communicating from a GitRepoInfoTask."""
@@ -337,7 +399,18 @@ class GitRepoItem(QtGui.QStandardItem):
self.setEditable(False)
self.setDragEnabled(False)
entry = GitRepoEntryManager.entry(path)
QtCore.QObject.connect(entry, SIGNAL(column), self.setText)
if column == Columns.STATUS:
QtCore.QObject.connect(entry, SIGNAL(column), self.set_status)
else:
QtCore.QObject.connect(entry, SIGNAL(column), self.setText)
def set_status(self, data):
icon, txt = data
if icon:
self.setIcon(QtGui.QIcon(icon))
else:
self.setIcon(QtGui.QIcon())
self.setText(txt)
class GitRepoNameItem(GitRepoItem):
+56 -10
View File
@@ -1,3 +1,5 @@
import os
from PyQt4 import QtGui
from PyQt4 import QtCore
from PyQt4.QtCore import Qt
@@ -7,7 +9,50 @@ import cola
import cola.utils
import cola.qtutils
from cola import signals
from cola.models import gitrepo
from cola.views import standard
from cola.classic.model import GitRepoNameItem
class RepoDialog(standard.Dialog):
def __init__(self, parent=None, update=True):
standard.Dialog.__init__(self, parent)
self.setObjectName('classic')
self.tree = RepoTreeView(parent)
self.setLayout(QtGui.QHBoxLayout())
self.layout().setMargin(1)
self.layout().addWidget(self.tree)
self.resize(720, 420)
self.connect(self, SIGNAL('updated'), self._updated_callback)
self.model = cola.model()
self.model.add_message_observer(self.model.message_updated,
self._model_updated)
cola.qtutils.add_close_action(self)
if update:
self._model_updated()
# Read-only mode property
mode = property(lambda self: self.model.mode)
def _model_updated(self):
"""Update the title with the current branch and directory name."""
self.emit(SIGNAL('updated'))
def _updated_callback(self):
branch = self.model.currentbranch
curdir = os.getcwd()
msg = 'Repository: %s\nBranch: %s' % (curdir, branch)
self.setToolTip(msg)
title = '%s [%s]' % (self.model.project, branch)
if self.mode in (self.model.mode_diff, self.model.mode_diff_expr):
title += ' *** diff mode***'
elif self.mode == self.model.mode_review:
title += ' *** review mode***'
elif self.mode == self.model.mode_amend:
title += ' *** amending ***'
self.setWindowTitle(title)
class RepoTreeView(QtGui.QTreeView):
@@ -15,8 +60,6 @@ class RepoTreeView(QtGui.QTreeView):
def __init__(self, parent=None):
QtGui.QTreeView.__init__(self, parent)
self.resize(720, 300)
self.setWindowTitle(self.tr('classic'))
self.setSortingEnabled(False)
self.setAllColumnsShowFocus(True)
self.setAlternatingRowColors(True)
@@ -199,10 +242,10 @@ class RepoTreeView(QtGui.QTreeView):
paths = self.selected_paths()
model = cola.model()
model_staged = set(model.staged)
model_modified = set(model.modified)
model_unmerged = set(model.unmerged)
model_untracked = set(model.untracked)
model_staged = cola.utils.add_parents(set(model.staged))
model_modified = cola.utils.add_parents(set(model.modified))
model_unmerged = cola.utils.add_parents(set(model.unmerged))
model_untracked =cola.utils.add_parents(set(model.untracked))
for path in paths:
if path in model_unmerged:
@@ -213,6 +256,8 @@ class RepoTreeView(QtGui.QTreeView):
staged.append(path)
elif path in model_modified:
modified.append(path)
else:
staged.append(path)
# Push the new selection into the model.
cola.selection_model().set_selection(staged, modified,
unmerged, untracked)
@@ -243,7 +288,7 @@ class RepoTreeView(QtGui.QTreeView):
"""Return the selected paths."""
items = map(self.model().itemFromIndex, self.selectedIndexes())
return [i.path for i in items
if i.type() == gitrepo.GitRepoNameItem.TYPE]
if i.type() == GitRepoNameItem.TYPE]
def selected_staged_paths(self, selection=None):
"""Return selected staged paths."""
@@ -280,13 +325,14 @@ class RepoTreeView(QtGui.QTreeView):
untracked = cola.utils.add_parents(set(model.untracked))
tracked = staged.union(modified)
return [p for p in selection
if p not in untracked or p in staged or p in modified]
if p not in untracked or p in tracked]
def _create_action(self, name, tooltip, slot, shortcut):
"""Create an action with a shortcut, tooltip, and callback slot."""
action = QtGui.QAction(self.tr(name), self)
action.setStatusTip(self.tr(tooltip))
action.setShortcutContext(Qt.WidgetWithChildrenShortcut)
if hasattr(Qt, 'WidgetWithChildrenShortcut'):
action.setShortcutContext(Qt.WidgetWithChildrenShortcut)
action.setShortcut(shortcut)
self.addAction(action)
self.connect(action, SIGNAL('triggered()'), slot)
+43 -33
View File
@@ -1,7 +1,7 @@
"""
Maps Qt signals to Command objects.
The command factory listens to the global notifier and
The command factory connects to the global notifier and
creates commands objects as registered signals are
encountered.
@@ -11,16 +11,14 @@ the undo and redo signals and manages the undo/redo stack.
"""
import cola
from cola import signals
from cola import errors
from cola.decorators import memoize
_factory = None
@memoize
def factory():
"""Return a static instance of the command factory."""
global _factory
if _factory:
return _factory
_factory = CommandFactory()
return _factory
return CommandFactory()
def SLOT(signal, *args, **opts):
@@ -34,40 +32,35 @@ def SLOT(signal, *args, **opts):
class CommandFactory(object):
def __init__(self):
def __init__(self, context=None):
"""Setup the undo/redo stacks and register for notifications."""
self.undoable = True
self.undostack = []
self.redostack = []
self.signal_to_command = {}
self.callbacks = {}
self.context = context
cola.notifier().listen(signals.undo, self.undo)
cola.notifier().listen(signals.redo, self.redo)
self.model = cola.model()
self.model.add_observer(self)
def has_command(self, signal):
return signal in self.signal_to_command
def add_command(self, signal, command):
"""Register a signal/command pair."""
self.signal_to_command[signal] = command
cola.notifier().listen(signal, self.cmdrunner(signal))
def notify(self, *params):
"""
Observe model changes.
def add_global_command(self, signal, command):
"""Register a global signal/command pair."""
self.add_command(signal, command)
cola.notifier().connect(signal, self.cmdrunner(signal))
This captures model parameters and maps them to signals that
are observed by the UIs.
def add_command_wrapper(self, cmd_wrapper):
self.callbacks.update(cmd_wrapper.callbacks)
"""
actions = {
'diff_text': SLOT(signals.diff_text, self.model.diff_text),
'commitmsg': SLOT(signals.editor_text, self.model.commitmsg),
'mode': SLOT(signals.mode, self.model.mode),
}
for param in params:
action = actions.get(param, lambda: None)
action()
def prompt_user(self, name, *args, **opts):
try:
return self.callbacks[name](*args, **opts)
except KeyError:
raise NotImplementedError('No callback for "%s' % name)
def clear(self):
"""Clear the undo and redo stacks."""
@@ -84,27 +77,44 @@ class CommandFactory(object):
"""Given a signal and arguments, run its corresponding command."""
cmdclass = self.signal_to_command[signal]
cmdobj = cmdclass(*args, **opts)
cmdobj.context = self.context
# TODO we disable undo/redo for now; views just need to
# inspect the stack and add menu entries when we enable it.
if self.undoable and cmdobj.is_undoable():
ok, result = self._do(cmdobj)
if ok and self.undoable and cmdobj.is_undoable():
self.undostack.append(cmdobj)
return cmdobj.do()
return result
def _do(self, cmdobj):
try:
result = cmdobj.do()
except errors.UsageError, e:
self.prompt_user(signals.information, e.title, e.message)
return False, None
else:
return True, result
def undo(self):
"""Undo the last command and add it to the redo stack."""
if self.undostack:
cmdobj = self.undostack.pop()
cmdobj.undo()
result = cmdobj.undo()
self.redostack.append(cmdobj)
return result
else:
print 'warning: undo stack is empty, doing nothing'
return None
def redo(self):
"""Redo the last command and add it to the undo stack."""
if self.redostack:
cmdobj = self.redostack.pop()
cmdobj.do()
self.undo.append(cmd)
ok, result = self._do(cmdobj)
if ok and cmdobj.is_undoable():
self.undo.append(cmdobj)
else:
self.redostack.push(cmdobj)
return result
else:
print 'warning: redo stack is empty, doing nothing'
+372 -106
View File
@@ -1,29 +1,54 @@
import os
import sys
import platform
from cStringIO import StringIO
import cola
from cola import i18n
from cola import core
from cola import errors
from cola import gitcfg
from cola import gitcmds
from cola import utils
from cola import signals
from cola import cmdfactory
from cola import difftool
from cola import version
from cola.diffparse import DiffParser
from cola.models import selection
_notifier = cola.notifier()
_factory = cmdfactory.factory()
_config = gitcfg.instance()
class Command(object):
"""Base class for all commands; provides the command pattern."""
def __init__(self, update=False):
class BaseCommand(object):
"""Base class for all commands; provides the command pattern"""
def __init__(self):
self.undoable = False
def is_undoable(self):
"""Can this be undone?"""
return self.undoable
def name(self):
"""Return this command's name."""
return self.__class__.__name__
def do(self):
raise NotImplementedError('%s.do() is unimplemented' % self.name())
def undo(self):
raise NotImplementedError('%s.undo() is unimplemented' % self.name())
class Command(BaseCommand):
"""Base class for commands that modify the main model"""
def __init__(self):
"""Initialize the command and stash away values for use in do()"""
# These are commonly used so let's make it easier to write new commands.
self.undoable = False
BaseCommand.__init__(self)
self.model = cola.model()
self.update = update
self.old_diff_text = self.model.diff_text
self.old_filename = self.model.filename
@@ -41,12 +66,6 @@ class Command(object):
self.model.set_filename(self.new_filename)
self.model.set_head(self.new_head)
self.model.set_mode(self.new_mode)
if self.update:
self.model.update_status()
def is_undoable(self):
"""Can this be undone?"""
return self.undoable
def undo(self):
"""Undo the operation."""
@@ -54,37 +73,12 @@ class Command(object):
self.model.set_filename(self.old_filename)
self.model.set_head(self.old_head)
self.model.set_mode(self.old_mode)
if self.update:
self.model.update_status()
def name(self):
"""Return this command's name."""
return self.__class__.__name__
class AddSignoff(Command):
"""Add a signed-off-by to the commit message."""
def __init__(self):
Command.__init__(self)
self.undoable = True
self.old_commitmsg = self.model.commitmsg
self.new_commitmsg = self.old_commitmsg
signoff = ('\nSigned-off-by: %s <%s>\n' %
(self.model.local_user_name, self.model.local_user_email))
if signoff not in self.new_commitmsg:
self.new_commitmsg += ('\n' + signoff)
def do(self):
self.model.set_commitmsg(self.new_commitmsg)
def undo(self):
self.model.set_commitmsg(self.old_commitmsg)
class AmendMode(Command):
"""Try to amend a commit."""
def __init__(self, amend):
Command.__init__(self, update=True)
Command.__init__(self)
self.undoable = True
self.skip = False
self.amending = amend
@@ -98,9 +92,10 @@ class AmendMode(Command):
# else, amend unchecked, regular commit
self.new_mode = self.model.mode_none
self.new_head = 'HEAD'
self.new_diff_text = ''
self.new_commitmsg = self.model.commitmsg
# If we're going back into new-commit-mode then search the
# undo stack for a previous amend-commid-mode and grab the
# undo stack for a previous amend-commit-mode and grab the
# commit message at that point in time.
if not _factory.undostack:
return
@@ -119,10 +114,10 @@ class AmendMode(Command):
"""Leave/enter amend mode."""
"""Attempt to enter amend mode. Do not allow this when merging."""
if self.amending:
if os.path.exists(self.model.git_repo_path('MERGE_HEAD')):
if os.path.exists(self.model.git.git_path('MERGE_HEAD')):
self.skip = True
_notifier.broadcast(signals.amend, False)
_notifier.broadcast(signals.information,
_factory.prompt_user(signals.information,
'Oops! Unmerged',
'You are in the middle of a merge.\n'
'You cannot amend while merging.')
@@ -131,17 +126,19 @@ class AmendMode(Command):
_notifier.broadcast(signals.amend, self.amending)
self.model.set_commitmsg(self.new_commitmsg)
Command.do(self)
self.model.update_file_status()
def undo(self):
if self.skip:
return
self.model.set_commitmsg(self.old_commitmsg)
Command.undo(self)
self.model.update_file_status()
class ApplyDiffSelection(Command):
def __init__(self, staged, selected, offset, selection, apply_to_worktree):
Command.__init__(self, update=True)
Command.__init__(self)
self.staged = staged
self.selected = selected
self.offset = offset
@@ -155,6 +152,7 @@ class ApplyDiffSelection(Command):
filename=self.model.filename,
cached=False,
branch=self.model.head)
status, output = \
parser.process_diff_selection(self.selected,
self.offset,
self.selection,
@@ -165,27 +163,71 @@ class ApplyDiffSelection(Command):
filename=self.model.filename,
cached=self.staged,
reverse=self.apply_to_worktree)
status, output = \
parser.process_diff_selection(self.selected,
self.offset,
self.selection,
apply_to_worktree=
self.apply_to_worktree)
_notifier.broadcast(signals.log_cmd, status, output)
# Redo the diff to show changes
if self.staged:
diffcmd = DiffStaged([self.model.filename])
else:
diffcmd = Diff([self.model.filename])
diffcmd.do()
self.model.update_status()
self.model.update_file_status()
class ApplyPatches(Command):
def __init__(self, patches):
Command.__init__(self)
patches.sort()
self.patches = patches
def do(self):
diff_text = ''
num_patches = len(self.patches)
orig_head = self.model.git.rev_parse('HEAD')
for idx, patch in enumerate(self.patches):
status, output = self.model.git.am(patch,
with_status=True,
with_stderr=True)
# Log the git-am command
_notifier.broadcast(signals.log_cmd, status, output)
if num_patches > 1:
diff = self.model.git.diff('HEAD^!', stat=True)
diff_text += 'Patch %d/%d - ' % (idx+1, num_patches)
diff_text += '%s:\n%s\n\n' % (os.path.basename(patch), diff)
diff_text += 'Summary:\n'
diff_text += self.model.git.diff(orig_head, stat=True)
# Display a diffstat
self.model.set_diff_text(diff_text)
self.model.update_file_status()
_factory.prompt_user(signals.information,
'Patch(es) Applied',
'%d patch(es) applied:\n\n%s' %
(len(self.patches),
'\n'.join(map(os.path.basename, self.patches))))
class HeadChangeCommand(Command):
"""Changes the model's current head."""
def __init__(self, treeish):
Command.__init__(self, update=True)
Command.__init__(self)
self.new_head = treeish
self.new_diff_text = ''
def do(self):
Command.do(self)
self.model.update_file_status()
class BranchMode(HeadChangeCommand):
"""Enter into diff-branch mode."""
@@ -194,10 +236,10 @@ class BranchMode(HeadChangeCommand):
self.old_filename = self.model.filename
self.new_filename = filename
self.new_mode = self.model.mode_branch
self.new_diff_text = self.model.diff_helper(filename=filename,
cached=False,
reverse=True,
branch=treeish)
self.new_diff_text = gitcmds.diff_helper(filename=filename,
cached=False,
reverse=True,
branch=treeish)
class Checkout(Command):
"""
A command object for git-checkout.
@@ -205,21 +247,25 @@ class Checkout(Command):
'argv' is handed off directly to git.
"""
def __init__(self, argv):
def __init__(self, argv, checkout_branch=False):
Command.__init__(self)
self.argv = argv
self.checkout_branch = checkout_branch
self.new_diff_text = ''
def do(self):
status, output = self.model.git.checkout(with_stderr=True,
with_status=True, *self.argv)
_notifier.broadcast(signals.log_cmd, status, output)
self.model.set_diff_text('')
self.model.update_status()
if self.checkout_branch:
self.model.update_status()
else:
self.model.update_file_status()
class CheckoutBranch(Checkout):
"""Checkout a branch."""
def __init__(self, branch):
def __init__(self, branch, checkout_branch=True):
Checkout.__init__(self, [branch])
@@ -231,16 +277,21 @@ class CherryPick(Command):
def do(self):
self.model.cherry_pick_list(self.commits)
self.model.update_file_status()
class ResetMode(Command):
"""Reset the mode and clear the model's diff text."""
def __init__(self):
Command.__init__(self, update=True)
Command.__init__(self)
self.new_mode = self.model.mode_none
self.new_head = 'HEAD'
self.new_diff_text = ''
def do(self):
Command.do(self)
self.model.update_file_status()
class Commit(ResetMode):
"""Attempt to create a new commit."""
@@ -262,6 +313,28 @@ class Commit(ResetMode):
_notifier.broadcast(signals.log_cmd, status, title+output)
class Ignore(Command):
"""Add files to .gitignore"""
def __init__(self, filenames):
Command.__init__(self)
self.filenames = filenames
def do(self):
new_additions = ''
for fname in self.filenames:
new_additions = new_additions + fname + '\n'
for_status = new_additions
if new_additions:
if os.path.exists('.gitignore'):
current_list = utils.slurp('.gitignore')
new_additions = new_additions + current_list
utils.write('.gitignore', new_additions)
_notifier.broadcast(signals.log_cmd,
0,
'Added to .gitignore:\n%s' % for_status)
self.model.update_file_status()
class Delete(Command):
"""Simply delete files."""
def __init__(self, filenames):
@@ -277,11 +350,11 @@ class Delete(Command):
os.remove(filename)
rescan=True
except:
_notifier.broadcast(signals.information,
_factory.prompt_user(signals.information,
'Error'
'Deleting "%s" failed.' % filename)
if rescan:
self.model.update_status()
self.model.update_file_status()
class DeleteBranch(Command):
"""Delete a git branch."""
@@ -303,6 +376,9 @@ class Diff(Command):
"""Perform a diff and set the model's current text."""
def __init__(self, filenames, cached=False):
Command.__init__(self)
# Guard against the list of files being empty
if not filenames:
return
opts = {}
if cached:
cached = not self.model.read_only()
@@ -311,9 +387,10 @@ class Diff(Command):
self.new_filename = filenames[0]
self.old_filename = self.model.filename
if not self.model.read_only():
self.new_mode = self.model.mode_worktree
self.new_diff_text = self.model.diff_helper(filename=self.new_filename,
cached=cached, **opts)
if self.model.mode != self.model.mode_amend:
self.new_mode = self.model.mode_worktree
self.new_diff_text = gitcmds.diff_helper(filename=self.new_filename,
cached=cached, **opts)
class DiffMode(HeadChangeCommand):
@@ -335,7 +412,7 @@ class Diffstat(Command):
def __init__(self):
Command.__init__(self)
diff = self.model.git.diff(self.model.head,
unified=self.model.diff_context,
unified=_config.get('diff.context', 3),
no_color=True,
M=True,
stat=True)
@@ -380,7 +457,9 @@ class Difftool(Command):
args = []
if self.staged and not self.model.read_only():
args.append('--cached')
args.extend([self.model.head, '--'])
if self.model.head != 'HEAD':
args.append(self.model.head)
args.append('--')
args.extend(self.filenames)
difftool.launch(args)
@@ -411,8 +490,8 @@ class FormatPatch(Command):
self.revs = revs
def do(self):
self.model.format_patch_helper(self.to_export, self.revs,
output='patches')
status, output = gitcmds.format_patchsets(self.to_export, self.revs)
_notifier.broadcast(signals.log_cmd, status, output)
class GrepMode(Command):
@@ -420,7 +499,7 @@ class GrepMode(Command):
"""Perform a git-grep."""
Command.__init__(self)
self.new_mode = self.model.mode_grep
self.new_diff_text = self.model.git.grep(txt, n=True)
self.new_diff_text = core.decode(self.model.git.grep(txt, n=True))
class LoadCommitMessage(Command):
@@ -433,17 +512,48 @@ class LoadCommitMessage(Command):
self.old_directory = self.model.directory
def do(self):
self.model.set_directory(os.path.dirname(self.path))
fh = open(self.path, 'r')
contents = core.decode(core.read_nointr(fh))
fh.close()
self.model.set_commitmsg(contents)
path = self.path
if not path or not os.path.isfile(path):
raise errors.UsageError('Error: cannot find commit template',
'%s: No such file or directory.' % path)
self.model.set_directory(os.path.dirname(path))
self.model.set_commitmsg(utils.slurp(path))
def undo(self):
self.model.set_commitmsg(self.old_commitmsg)
self.model.set_directory(self.old_directory)
class LoadCommitTemplate(LoadCommitMessage):
"""Loads the commit message template specified by commit.template."""
def __init__(self):
LoadCommitMessage.__init__(self, _config.get('commit.template'))
def do(self):
if self.path is None:
raise errors.UsageError('Error: unconfigured commit template',
'A commit template has not been configured.\n'
'Use "git config" to define "commit.template"\n'
'so that it points to a commit template.')
return LoadCommitMessage.do(self)
class LoadPreviousMessage(Command):
"""Try to amend a commit."""
def __init__(self, sha1):
Command.__init__(self)
self.sha1 = sha1
self.old_commitmsg = self.model.commitmsg
self.new_commitmsg = self.model.prev_commitmsg(sha1)
self.undoable = True
def do(self):
self.model.set_commitmsg(self.new_commitmsg)
def undo(self):
self.model.set_commitmsg(self.old_commitmsg)
class Mergetool(Command):
"""Launch git-mergetool on a list of paths."""
def __init__(self, paths):
@@ -453,56 +563,143 @@ class Mergetool(Command):
def do(self):
if not self.paths:
return
if version.check('mergetool-no-prompt',
self.model.git.version().split()[-1]):
utils.fork(['git', 'mergetool', '--no-prompt', '--'] + self.paths)
else:
utils.fork(['xterm', '-e', 'git', 'mergetool', '--'] + self.paths)
utils.fork(['git', 'mergetool', '--no-prompt', '--'] + self.paths)
class OpenRepo(Command):
"""Launches git-cola on a repo."""
def __init__(self, dirname):
Command.__init__(self)
self.new_directory = utils.quote_repopath(dirname)
self.new_directory = dirname
def do(self):
self.model.set_directory(self.new_directory)
utils.fork(['python', sys.argv[0], '--repo', self.new_directory])
utils.fork([sys.executable, sys.argv[0], '--repo', self.new_directory])
class Clone(Command):
"""Clones a repository and launches a new cola session."""
def __init__(self, url, destdir):
"""Clones a repository and optionally spawns a new cola session."""
def __init__(self, url, destdir, spawn=True):
Command.__init__(self)
self.url = url
self.new_directory = utils.quote_repopath(destdir)
self.new_directory = destdir
self.spawn = spawn
def do(self):
self.model.git.clone(self.url, self.new_directory,
with_stderr=True, with_status=True)
utils.fork(['python', sys.argv[0], '--repo', self.new_directory])
if self.spawn:
utils.fork(['python', sys.argv[0], '--repo', self.new_directory])
rescan = 'rescan'
class Rescan(Command):
"""Rescans for changes."""
def __init__(self):
Command.__init__(self, update=True)
def do(self):
self.model.update_status(update_index=True)
class ReviewBranchMode(Command):
"""Enter into review-branch mode."""
def __init__(self, branch):
Command.__init__(self, update=True)
Command.__init__(self)
self.new_mode = self.model.mode_review
self.new_head = self.model.merge_base_to(branch)
self.new_head = gitcmds.merge_base_parent(branch)
self.new_diff_text = ''
def do(self):
Command.do(self)
self.model.update_status()
class RunConfigAction(Command):
"""Run a user-configured action, typically from the "Tools" menu"""
def __init__(self, name):
Command.__init__(self)
self.name = name
self.model = cola.model()
def do(self):
for env in ('FILENAME', 'REVISION', 'ARGS'):
try:
del os.environ[env]
except KeyError:
pass
rev = None
args = None
opts = _config.get_guitool_opts(self.name)
cmd = opts.get('cmd')
if 'title' not in opts:
opts['title'] = cmd
if 'prompt' not in opts or opts.get('prompt') is True:
prompt = i18n.gettext('Are you sure you want to run %s?') % cmd
opts['prompt'] = prompt
if opts.get('needsfile'):
filename = selection.filename()
if not filename:
_factory.prompt_user(signals.information,
'Please select a file',
'"%s" requires a selected file' % cmd)
return
os.environ['FILENAME'] = filename
if opts.get('revprompt') or opts.get('argprompt'):
while True:
ok = _factory.prompt_user(signals.run_config_action, cmd, opts)
if not ok:
return
rev = opts.get('revision')
args = opts.get('args')
if opts.get('revprompt') and not rev:
msg = ('Invalid revision:\n\n'
'Revision expression is empty')
title = 'Oops!'
_factory.prompt_user(signals.information, title, msg)
continue
break
elif opts.get('confirm'):
title = os.path.expandvars(opts.get('title'))
prompt = os.path.expandvars(opts.get('prompt'))
if not _factory.prompt_user(signals.question, title, prompt):
return
if rev:
os.environ['REVISION'] = rev
if args:
os.environ['ARGS'] = args
title = os.path.expandvars(cmd)
_notifier.broadcast(signals.log_cmd, 0, 'running: ' + title)
cmd = ['sh', '-c', cmd]
if opts.get('noconsole'):
status, out, err = utils.run_command(cmd, flag_error=False)
else:
status, out, err = _factory.prompt_user(signals.run_command,
title, cmd)
_notifier.broadcast(signals.log_cmd, status,
'stdout: %s\nstatus: %s\nstderr: %s' %
(out.rstrip(), status, err.rstrip()))
if not opts.get('norescan'):
self.model.update_status()
return status
class SetDiffText(Command):
def __init__(self, text):
Command.__init__(self)
self.undoable = True
self.new_diff_text = text
class ShowUntracked(Command):
"""Show an untracked file."""
# We don't actually do anything other than set the mode right now.
# We could probably check the mimetype for the file and handle things
# TODO check the mimetype for the file and handle things
# generically.
def __init__(self, filenames):
Command.__init__(self)
@@ -510,6 +707,33 @@ class ShowUntracked(Command):
# TODO new_diff_text = utils.file_preview(filenames[0])
class SignOff(Command):
def __init__(self):
Command.__init__(self)
self.undoable = True
self.old_commitmsg = self.model.commitmsg
def do(self):
signoff = self.signoff()
if signoff in self.model.commitmsg:
return
self.model.set_commitmsg(self.model.commitmsg + '\n' + signoff)
def undo(self):
self.model.set_commitmsg(self.old_commitmsg)
def signoff(self):
try:
import pwd
user = pwd.getpwuid(os.getuid()).pw_name
except ImportError:
user = os.getenv('USER', 'unknown')
name = _config.get('user.name', user)
email = _config.get('user.email', '%s@%s' % (user, platform.node()))
return '\nSigned-off-by: %s <%s>' % (name, email)
class Stage(Command):
"""Stage a set of paths."""
def __init__(self, paths):
@@ -535,6 +759,7 @@ class StageUntracked(Stage):
Stage.__init__(self, None)
self.paths = self.model.untracked
class Tag(Command):
"""Create a tag object."""
def __init__(self, name, revision, sign=False, message=''):
@@ -546,28 +771,35 @@ class Tag(Command):
def do(self):
log_msg = 'Tagging: "%s" as "%s"' % (self._revision, self._name)
opts = {}
if self._message:
opts['F'] = self.model.tmp_filename()
utils.write(opts['F'], self._message)
if self._sign:
log_msg += ', GPG-signed'
path = cola.model().tmp_filename()
utils.write(path, self._message)
status, output = cola.model().git.tag(self._name,
self._revision,
s=True,
F=path,
with_status=True,
with_stderr=True)
os.unlink(path)
opts['s'] = True
status, output = self.model.git.tag(self._name,
self._revision,
with_status=True,
with_stderr=True,
**opts)
else:
status, output = cola.model().git.tag(self._name,
self._revision,
with_status=True,
with_stderr=True)
opts['a'] = bool(self._message)
status, output = self.model.git.tag(self._name,
self._revision,
with_status=True,
with_stderr=True,
**opts)
if 'F' in opts:
os.unlink(opts['F'])
if output:
log_msg += '\nOutput:\n%s' % output
_notifier.broadcast(signals.log_cmd, status, log_msg)
if status == 0:
cola.model().update_status()
self.model.update_status()
class Unstage(Command):
@@ -584,9 +816,6 @@ class Unstage(Command):
class UnstageAll(Command):
"""Unstage all files; resets the index."""
def __init__(self):
Command.__init__(self, update=True)
def do(self):
self.model.unstage_all()
@@ -608,8 +837,18 @@ class UntrackedSummary(Command):
if untracked:
io.write('# possible .gitignore rule%s:\n' % suffix)
for u in untracked:
io.write('/%s\n' % u)
self.new_diff_text = io.getvalue()
io.write('/'+core.encode(u))
self.new_diff_text = core.decode(io.getvalue())
class UpdateStatus(Command):
"""Update the status of a list of files."""
def __init__(self, files):
Command.__init__(self)
self.files = files
def do(self):
self.model.update_status_of_files(self.files)
class VisualizeAll(Command):
@@ -640,6 +879,25 @@ class VisualizePaths(Command):
utils.fork(self.argv)
visualize_revision = 'visualize_revision'
class VisualizeRevision(Command):
"""Visualize a specific revision."""
def __init__(self, revision, paths=None):
Command.__init__(self)
self.revision = revision
self.paths = paths
def do(self):
argv = [self.model.history_browser()]
if self.revision:
argv.append(self.revision)
if self.paths:
argv.append('--')
argv.extend(self.paths)
utils.fork(argv)
def register():
"""
Register signal mappings with the factory.
@@ -649,9 +907,9 @@ def register():
"""
signal_to_command_map = {
signals.add_signoff: AddSignoff,
signals.apply_diff_selection: ApplyDiffSelection,
signals.amend_mode: AmendMode,
signals.apply_diff_selection: ApplyDiffSelection,
signals.apply_patches: ApplyPatches,
signals.branch_mode: BranchMode,
signals.clone: Clone,
signals.checkout: Checkout,
@@ -669,14 +927,20 @@ def register():
signals.edit: Edit,
signals.format_patch: FormatPatch,
signals.grep: GrepMode,
signals.ignore: Ignore,
signals.load_commit_message: LoadCommitMessage,
signals.load_commit_template: LoadCommitTemplate,
signals.load_previous_message: LoadPreviousMessage,
signals.modified_summary: Diffstat,
signals.mergetool: Mergetool,
signals.open_repo: OpenRepo,
signals.rescan: Rescan,
signals.reset_mode: ResetMode,
signals.review_branch_mode: ReviewBranchMode,
signals.run_config_action: RunConfigAction,
signals.set_diff_text: SetDiffText,
signals.show_untracked: ShowUntracked,
signals.signoff: SignOff,
signals.stage: Stage,
signals.stage_modified: StageModified,
signals.stage_untracked: StageUntracked,
@@ -686,10 +950,12 @@ def register():
signals.unstage_all: UnstageAll,
signals.unstage_selected: UnstageSelected,
signals.untracked_summary: UntrackedSummary,
signals.update_status: UpdateStatus,
signals.visualize_all: VisualizeAll,
signals.visualize_current: VisualizeCurrent,
signals.visualize_paths: VisualizePaths,
signals.visualize_revision: VisualizeRevision,
}
for signal, cmd in signal_to_command_map.iteritems():
_factory.add_command(signal, cmd)
_factory.add_global_command(signal, cmd)
+14
View File
@@ -0,0 +1,14 @@
try:
set = set
except NameError:
from sets import Set as set
set = set
try:
import hashlib
except ImportError:
import md5
class hashlib(object):
@staticmethod
def new(*args):
return md5.new()
+6 -4
View File
@@ -5,6 +5,7 @@ import sys
from PyQt4 import QtGui
from cola import core
from cola import utils
from cola import qtutils
from cola.qobserver import QObserver
@@ -19,8 +20,8 @@ def save_bookmark():
"""
model = settings.SettingsManager.settings()
model.add_bookmark(os.getcwd())
model.save()
model.add_bookmark(core.decode(os.getcwd()))
settings.SettingsManager.save()
qtutils.information("Bookmark Saved")
def manage_bookmarks():
@@ -31,6 +32,7 @@ def manage_bookmarks():
ctl = BookmarkController(model, view)
view.show()
class BookmarkController(QObserver):
"""Handles interactions with the bookmarks dialog
"""
@@ -45,7 +47,7 @@ class BookmarkController(QObserver):
def save(self):
"""Saves the bookmarks settings and exits"""
self.model.save()
settings.SettingsManager.save()
self.view.accept()
def open(self):
@@ -55,7 +57,7 @@ class BookmarkController(QObserver):
if not selection:
return
for item in selection:
utils.fork(['git', 'cola', item])
utils.fork([sys.executable, sys.argv[0], '--repo', item])
def delete(self):
"""Removes a bookmark from the bookmarks list"""
+25 -16
View File
@@ -1,44 +1,42 @@
"""This controller handles the compare commits dialog."""
import os
from PyQt4 import QtGui
import cola
from cola import utils
from cola import qtutils
from cola import difftool
from cola import gitcmds
from cola.qobserver import QObserver
from cola.models.compare import CompareModel
from cola.models.compare import BranchCompareModel
from cola.views import compare
from cola.controllers.repobrowser import select_file_from_repo
from cola.views import compare as vcompare
from cola.widgets.browse import BrowseDialog
def compare_file():
"""Launches a dialog for comparing revisions touching a file path"""
model = cola.model()
parent = QtGui.QApplication.instance().activeWindow()
filename = select_file_from_repo()
filename = BrowseDialog.select_file('HEAD')
if not filename:
return
compare(filename)
def compare(filename=None):
"""Launches a dialog for comparing a pair of commits"""
parent = QtGui.QApplication.instance().activeWindow()
model = CompareModel()
view = compare.CompareView(parent)
view = vcompare.CompareView(parent)
ctl = CompareController(model, view, filename)
view.show()
return ctl
def branch_compare():
"""Launches a dialog for comparing a pair of branches"""
model = BranchCompareModel()
parent = QtGui.QApplication.instance().activeWindow()
view = compare.BranchCompareView(parent)
view = vcompare.BranchCompareView(parent)
ctl = BranchCompareController(model, view)
view.show()
return ctl
class BranchCompareController(QObserver):
"""Provides a dialog for comparing local and remote git branches"""
@@ -124,9 +122,20 @@ class BranchCompareController(QObserver):
"""
if branch == BranchCompareController.BRANCH_POINT:
# Compare against the branch point so find the merge-base
branch = self.model.currentbranch
remote = gitcmds.corresponding_remote_ref()
return self.model.git.merge_base(branch, remote)
branch = gitcmds.current_branch()
tracked_branch = gitcmds.tracked_branch()
if tracked_branch:
return self.model.git.merge_base(branch, tracked_branch)
else:
remote_branches = gitcmds.branch_list(remote=True)
remote_branch = 'origin/%s' % branch
if remote_branch in remote_branches:
return self.model.git.merge_base(branch, remote_branch)
elif 'origin/master' in remote_branches:
return self.model.git.merge_base(branch, 'origin/master')
else:
return 'HEAD'
else:
# Compare against the remote branch
return branch
@@ -304,11 +313,11 @@ class CompareController(QObserver):
# get the changed files list
start = self.model.revision_start
end = self.model.revision_end
files = self.model.changed_files(start, end)
files = gitcmds.changed_files(start, end)
# get the old name of any renamed files, and prune them
# from the changes list
renamed_files = self.model.renamed_files(start, end)
renamed_files = gitcmds.renamed_files(start, end)
for renamed in renamed_files:
try:
files.remove(renamed)
+4 -4
View File
@@ -1,10 +1,10 @@
"""This controller handles the create branch dialog."""
import os
from PyQt4 import QtGui
import cola
from cola import serializer
from cola import gitcmds
from cola import utils
from cola import qtutils
from cola.views import createbranch
@@ -12,7 +12,7 @@ from cola.qobserver import QObserver
def create_new_branch(revision=''):
"""Launches a dialog for creating a new branch"""
model = cola.model().clone()
model = serializer.clone(cola.model())
parent = QtGui.QApplication.instance().activeWindow()
view = createbranch.CreateBranchView(parent)
ctl = CreateBranchController(model, view)
@@ -55,7 +55,7 @@ class CreateBranchController(QObserver):
qtutils.information('warning', msg)
return
# Whether we should prompt the user for lost commits
commits = self.model.rev_list_range(revision, branch)
commits = gitcmds.rev_list_range(revision, branch)
check_branch = bool(commits)
if check_branch:
+7 -4
View File
@@ -10,17 +10,20 @@ from cola.models import tag
from cola.views import createtag
def create_tag():
def create_tag(revision=''):
"""Entry point for external callers."""
app = QtGui.QApplication.instance()
model = tag.TagModel()
if revision:
model.revision = [revision]
view = createtag.CreateTag(parent=app.activeWindow())
ctl = CreateTagController(view)
ctl = CreateTagController(model, view)
view.show()
class CreateTagController(qobserver.QObserver):
def __init__(self, view):
qobserver.QObserver.__init__(self, tag.TagModel(), view)
def __init__(self, model, view):
qobserver.QObserver.__init__(self, model, view)
self.add_observables('tag_name', 'tag_msg', 'revision', 'sign_tag')
self.add_callbacks(create_button=self.create_tag)
self.refresh_view()
-47
View File
@@ -1,47 +0,0 @@
"""Provides the main application controller."""
from PyQt4 import QtGui
import cola
from cola import qtutils
from cola import version
from cola.qobserver import QObserver
from cola.views import log
class MainController(QObserver):
"""Manage interactions between models and views."""
def __init__(self, model, view):
"""Initializes the MainController's internal data."""
QObserver.__init__(self, model, view)
# Binds model params to their equivalent view widget
self.add_observables('commitmsg')
# When a model attribute changes, this runs a specific action
self.add_actions(global_cola_fontdiff = self.update_diff_font)
self.add_actions(global_cola_tabwidth = self.update_tab_width)
# Route events here
self._init_log_window()
self.refresh_view('global_cola_fontdiff') # Update the diff font
def update_diff_font(self):
"""Updates the diff font based on the configured value."""
qtutils.set_diff_font(qtutils.logger())
qtutils.set_diff_font(self.view.display_text)
qtutils.set_diff_font(self.view.commitmsg)
def update_tab_width(self):
"""Implement the variable-tab-width setting."""
tab_width = self.model.cola_config('tabwidth')
display_font = self.view.display_text.font()
space_width = QtGui.QFontMetrics(display_font).width(' ')
self.view.display_text.setTabStopWidth(tab_width * space_width)
def _init_log_window(self):
"""Initialize the logging subwindow."""
branch = self.model.currentbranch
qtutils.log(0, self.model.git_version +
'\ncola version ' + version.version())
-96
View File
@@ -1,96 +0,0 @@
"""This controller handles the merge dialog."""
from PyQt4 import QtGui
from PyQt4.Qt import Qt
import cola
from cola import utils
from cola import qtutils
from cola.qobserver import QObserver
from cola.views.merge import MergeView
def abort_merge():
"""Prompts before aborting a merge in progress
"""
txt = ('Abort merge?\n'
'Aborting the current merge will cause '
'*ALL* uncommitted changes to be lost.\n\n'
'Continue with aborting the current merge?')
parent = QtGui.QApplication.instance().activeWindow()
model = cola.model()
answer = qtutils.question(parent, 'Abort Merge?', txt, default=False)
if answer:
model.abort_merge()
def local_merge():
"""Provides a dialog for merging branches"""
model = cola.model().clone()
parent = QtGui.QApplication.instance().activeWindow()
view = MergeView(parent)
ctl = MergeController(model, view)
view.show()
class MergeController(QObserver):
"""Provide control to the merge dialog"""
def __init__(self, model, view):
QObserver.__init__(self, model, view)
# Set the current branch label
self.view.set_branch(self.model.currentbranch)
self.add_observables('revision', 'revisions')
self.add_callbacks(radio_local = self.radio_callback,
radio_remote = self.radio_callback,
radio_tag = self.radio_callback,
revisions = self.revision_selected,
button_viz = self.viz_revision,
button_merge = self.merge_revision)
self.model.set_revisions(self.model.local_branches)
def revisions(self):
"""Retrieve candidate items to merge"""
if self.view.radio_local.isChecked():
return self.model.local_branches
elif self.view.radio_remote.isChecked():
return self.model.remote_branches
elif self.view.radio_tag.isChecked():
return self.model.tags
return []
def revision_selected(self, *args):
"""Update the revision field when a list item is selected"""
revlist = self.revisions()
widget = self.view.revisions
row, selected = qtutils.selected_row(widget)
if selected and row < len(revlist):
revision = revlist[row]
self.model.set_revision(revision)
def radio_callback(self):
"""Update the revision list whenever a radio button is clicked"""
self.model.set_revisions(self.revisions())
def merge_revision(self):
"""Merge the selected revision/branch"""
revision = self.model.revision
if not revision:
qtutils.information('No Revision Specified',
'You must specify a revision to merge')
return
no_commit = not(self.view.checkbox_commit.isChecked())
squash = self.view.checkbox_squash.isChecked()
msg = self.model.merge_message()
qtutils.log(*self.model.git.merge('-m'+msg,
revision,
strategy='recursive',
no_commit=no_commit,
squash=squash,
with_stderr=True,
with_status=True))
self.view.accept()
def viz_revision(self):
"""Launch a gitk-like viewer on the selection revision"""
revision = self.model.revision
browser = self.model.history_browser()
utils.fork([browser, revision])
-111
View File
@@ -1,111 +0,0 @@
"""This module provides the controller for the options gui
"""
from PyQt4 import QtGui
import cola
from cola import qtutils
from cola.views import option
from cola.qobserver import QObserver
def update_options():
"""Launch the options window given a model and parent widget."""
parent = QtGui.QApplication.instance().activeWindow()
view = option.OptionsView(parent)
ctl = OptionsController(view)
view.show()
return view.exec_() == QtGui.QDialog.Accepted
class OptionsController(QObserver):
"""Provides control to the options dialog."""
def __init__(self, view):
## operate on a clone of the original model
QObserver.__init__(self, cola.model().clone(), view)
## used to restore original values when cancelling
self._backup_model = cola.model().clone()
## config params modified by the gui
self.add_observables('local_user_email',
'local_user_name',
'local_merge_summary',
'local_merge_diffstat',
'local_merge_verbosity',
'local_gui_diffcontext',
'global_user_email',
'global_user_name',
'global_merge_keepbackup',
'global_merge_summary',
'global_merge_diffstat',
'global_merge_verbosity',
'global_gui_editor',
'global_merge_tool',
'global_diff_tool',
'global_gui_diffcontext',
'global_gui_historybrowser',
'global_cola_fontdiff_size',
'global_cola_fontdiff',
'global_cola_savewindowsettings',
'global_cola_tabwidth')
# Refresh before registering callbacks to avoid extra notifications
self.refresh_view()
# Register actions
self.add_actions(global_cola_fontdiff = self.tell_parent_model)
self.add_callbacks(global_cola_fontdiff_size = self.update_size)
self.add_actions(global_cola_tabwidth = self.tell_parent_model)
self.add_callbacks(save_button = self.save_settings)
self.connect(self.view, 'rejected()', self.restore_settings)
def refresh_view(self):
"""Apply the configured font and update widgets."""
# The fixed-width console font
qtutils.set_diff_font(self.view.global_cola_fontdiff)
# Label the group box around the local repository
self.view.local_groupbox.setTitle(unicode(self.tr('%s Repository'))
% self.model.project)
QObserver.refresh_view(self)
def save_settings(self):
"""Save updated config variables back to git."""
params_to_save = []
params = self.model.config_params()
for param in params:
value = self.model.param(param)
backup = self._backup_model.param(param)
if value != backup:
params_to_save.append(param)
for param in params_to_save:
self.model.save_config_param(param)
# Update the main model with any changed parameters
cola.model().copy_params(self.model, params_to_save)
self.view.done(QtGui.QDialog.Accepted)
def restore_settings(self):
"""Reverts any changes done in the Options dialog."""
params = (self._backup_model.config_params() +
['global_cola_fontdiff_size'])
self.model.copy_params(self._backup_model, params)
self.tell_parent_model()
def tell_parent_model(self,*rest):
"""Notifies the main app's model about changed parameters"""
params= ('global_cola_fontdiff',
'global_cola_fontdiff_size',
'global_cola_savewindowsettings',
'global_cola_tabwidth')
for param in params:
cola.model().set_param(param, self.model.param(param))
def update_size(self, *rest):
"""Updates fonts whenever font sizes change"""
# The fixed-width console font combobox
font = str(self.view.global_cola_fontdiff.currentFont().toString())
default = self.model.global_cola_fontdiff or font
self.model.apply_diff_font_size(default)
self.tell_parent_model()
+123 -20
View File
@@ -1,10 +1,15 @@
"""This controller handles the remote dialog."""
import os
from PyQt4.QtGui import QDialog
import fnmatch
from PyQt4 import QtGui
from PyQt4 import QtCore
from PyQt4.QtCore import Qt
from PyQt4.QtCore import SIGNAL
import cola
from cola import serializer
from cola import gitcmds
from cola import utils
from cola import qtutils
from cola.views import remote
@@ -13,7 +18,7 @@ from cola.qobserver import QObserver
def remote_action(parent, action):
"""Launches fetch/push/pull dialogs."""
# TODO: subclass model
model = cola.model()
model = serializer.clone(cola.model())
model.remotename = ''
model.tags_checkbox = False
model.rebase_checkbox = False
@@ -23,6 +28,7 @@ def remote_action(parent, action):
controller = RemoteController(model, view, action)
view.show()
class RemoteController(QObserver):
"""Provides control for the remote-action dialog."""
def __init__(self, model, view, action):
@@ -44,6 +50,12 @@ class RemoteController(QObserver):
'push': self.gen_remote_callback(self.model.push_helper),
'pull': self.gen_remote_callback(self.model.pull_helper),
} [action]
self._tasks = []
self.progress = QtGui.QProgressDialog(self.view)
self.progress.setRange(0, 0)
self.progress.setCancelButton(None)
self.progress.setWindowTitle('git ' + action)
self.progress.setWindowModality(Qt.WindowModal)
"""Callbacks corresponding to the 3 (fetch/push/pull) modes"""
self.add_actions(remotes = self.display_remotes)
@@ -51,6 +63,8 @@ class RemoteController(QObserver):
remotes = self.update_remotes,
local_branches = self.update_local_branches,
remote_branches = self.update_remote_branches)
self.view.connect(self.view, SIGNAL('action_completed'),
self._action_completed)
self.refresh_view()
remotes = self.model.remotes
if 'origin' in remotes:
@@ -61,22 +75,31 @@ class RemoteController(QObserver):
if self.view.select_first_remote():
self.model.set_remotename(remotes[0])
# Trim the remote list to just the default remote
self.update_remotes()
# Default to "git fetch origin master"
if action == 'fetch' or action == 'pull':
self.model.set_local_branch('')
self.model.set_remote_branch('')
return
# Select the current branch by default for push
if action != 'push':
return
branches = self.model.local_branches
branch = self.model.currentbranch
if branch not in branches:
return
idx = branches.index(branch)
if self.view.select_local_branch(idx):
self.model.set_local_branch(branch)
if action == 'push':
branch = self.model.currentbranch
try:
idx = self.model.local_branches.index(branch)
except ValueError:
return
if self.view.select_local_branch(idx):
self.model.set_local_branch(branch)
self.model.set_remote_branch('')
def display_remotes(self, widget):
"""Display the available remotes in a listwidget"""
displayed = []
for remotename in self.model.remotes:
url = self.model.remote_url(remotename)
url = self.model.remote_url(remotename, self.action)
display = ('%s\t(%s %s)'
% (remotename, unicode(self.tr('URL:')), url))
displayed.append(display)
@@ -92,6 +115,20 @@ class RemoteController(QObserver):
self.model.set_remotename(selection)
self.view.remotename.selectAll()
if self.action != 'pull':
pass
all_branches = gitcmds.branch_list(remote=True)
branches = []
pat = selection + '/*'
for branch in all_branches:
if fnmatch.fnmatch(branch, pat):
branches.append(branch)
if branches:
self.model.set_remote_branches(branches)
else:
self.model.set_remote_branches(all_branches)
self.model.set_remote_branch('')
def update_local_branches(self,*rest):
"""Update the local/remote branch names when a branch is selected"""
branches = self.model.local_branches
@@ -141,7 +178,20 @@ class RemoteController(QObserver):
errmsg = self.tr('No repository selected.')
qtutils.log(1, errmsg)
return
remote, kwargs = self.common_args()
action = self.action
# Check if we're about to create a new branch and warn.
if action == 'push' and not self.model.remote_branch:
branch = self.model.local_branch
candidate = '%s/%s' % (remote, branch)
if candidate not in self.model.remote_branches:
msg = ('Branch "' + branch + '" does not exist in ' +
remote + '.\n\nCreate a new branch?')
if not qtutils.question(self.view, 'Create New Branch?',
msg, default=False):
return
if not self.model.ffwd_only_checkbox:
if action == 'fetch':
msg = ('Non-fast-forward fetch overwrites local '
@@ -155,11 +205,64 @@ class RemoteController(QObserver):
if not qtutils.question(self.view,
'Force %s?' % action.title(), msg, default=False):
return
remote, kwargs = self.common_args()
status, output = modelaction(remote, **kwargs)
if not output: # git fetch --tags --verbose doesn't print anything...
output = self.tr('Already up-to-date.')
# Force the status to 1 so that we always display the log
qtutils.log(1, output)
self.view.accept()
# Disable the GUI by default
self.view.setEnabled(False)
self.progress.setEnabled(True)
QtGui.QApplication.setOverrideCursor(Qt.WaitCursor)
# Show a nice progress bar
self.progress.setLabelText('Updating...')
self.progress.show()
# Use a thread to update in the background
task = ActionTask(self.view, modelaction, remote, kwargs)
self._tasks.append(task)
QtCore.QThreadPool.globalInstance().start(task)
return remote_callback
def _action_completed(self, task, status, output):
# Grab the results of the action and finish up
if task in self._tasks:
self._tasks.remove(task)
if not output: # git fetch --tags --verbose doesn't print anything...
output = self.tr('Already up-to-date.')
# Force the status to 1 so that we always display the log
qtutils.log(1, output)
self.progress.close()
QtGui.QApplication.restoreOverrideCursor()
if status != 0 and self.action == 'push':
message = 'Error pushing to "%s".\n\nPull first?' % self.model.remotename
qtutils.critical('Push Error',
message=message, details=output)
else:
title = self.view.windowTitle()
if status == 0:
result = 'succeeded'
else:
result = 'returned exit status %d' % status
message = '"git %s" %s' % (self.action, result)
qtutils.information(title,
parent=self.view,
message=message, details=output)
self.view.accept()
class ActionTask(QtCore.QRunnable):
def __init__(self, sender, modelaction, remote, kwargs):
QtCore.QRunnable.__init__(self)
self._sender = sender
self._modelaction = modelaction
self._remote = remote
self._kwargs = kwargs
def run(self):
"""Runs the model action and captures the result"""
status, output = self._modelaction(self._remote, **self._kwargs)
self._sender.emit(SIGNAL('action_completed'), self, status, output)
-232
View File
@@ -1,232 +0,0 @@
"""This controller handles the repository file browser."""
import os
from PyQt4 import QtGui
import cola
from cola import utils
from cola import resources
from cola import qtutils
from cola.views.selectcommits import SelectCommitsView
from cola.qobserver import QObserver
def select_file_from_repo():
"""Launche a dialog to selecting a filename from a branch."""
model = cola.model().clone()
parent = QtGui.QApplication.instance().activeWindow()
view = SelectCommitsView(parent, syntax=False)
controller = RepoBrowserController(model, view,
title='Select File',
get_file=True)
view.show()
if view.exec_() == QtGui.QDialog.Accepted:
return controller.filename
else:
return None
def browse_git_branch(branch):
"""Launch a dialog to browse files in a specific branch."""
if not branch:
return
# Clone the model to allow opening multiple browsers
# with different sets of data
model = cola.model().clone()
model.set_currentbranch(branch)
parent = QtGui.QApplication.instance().activeWindow()
view = SelectCommitsView(parent, syntax=False)
controller = RepoBrowserController(model, view)
view.show()
return view.exec_() == QtGui.QDialog.Accepted
class RepoBrowserController(QObserver):
"""Provides control to the Repository Browser."""
def __init__(self, model, view,
title='File Browser', get_file=False):
QObserver.__init__(self, model, view)
self.get_file = get_file
"""Whether we should returns a selected file"""
self.filename = None
"""The last selected filename"""
view.setWindowTitle(title)
self.add_signals('itemSelectionChanged()', view.commit_list,)
self.add_actions(directory = self.action_directory_changed)
self.add_callbacks(commit_list = self.item_changed)
view.commit_list.contextMenuEvent = self.context_menu_event
# Start at the root of the tree
model.set_directory('')
self.refresh_view()
def context_menu_event(self, event):
"""Generate a context menu for the repository browser."""
menu = QtGui.QMenu(self.view);
menu.addAction(self.tr('Blame'), self.blame)
menu.exec_(self.view.commit_list.mapToGlobal(event.pos()))
def blame(self):
"""Show git-blame output for a file path."""
current = self.view.commit_list.currentRow()
item = self.view.commit_list.item(current)
if item is None or not item.isSelected():
return
directories = self.model.directories
directory_entries = self.model.directory_entries
if current < len(directories):
# ignore directories
return
idx = current - len(directories)
if idx >= len(self.model.subtree_sha1s):
return
objtype, sha1, name = self.model.subtree_node(idx)
curdir = self.model.directory
if curdir:
filename = os.path.join(curdir, name)
else:
filename = name
blame = self.model.git.blame(self.model.currentbranch, filename)
self.view.commit_text.setText(blame)
######################################################################
# Actions
def action_directory_changed(self):
"""Called in response to a change in the model's directory."""
self.model.init_browser_data()
self._display_items()
######################################################################
# Qt callbacks
def item_changed(self,*rest):
"""Called when the current item changes"""
current = self.view.commit_list.currentRow()
item = self.view.commit_list.item(current)
if item is None or not item.isSelected():
self.view.revision.setText('')
self.view.commit_text.setText('')
return
directories = self.model.directories
directory_entries = self.model.directory_entries
if current < len(directories):
# This is a directory...
self.filename = None
dirent = directories[current]
if dirent != '..':
# This is a real directory for which
# we have child entries
entries = directory_entries[dirent]
else:
# This is '..' which is a special case
# since it doesn't really exist
entries = []
self.view.commit_text.setText('\n'.join(entries))
self.view.revision.setText('')
else:
# This is a file entry. The current row is absolute,
# so get a relative index by subtracting the number
# of directory entries
idx = current - len(directories)
if idx >= len(self.model.subtree_sha1s):
# This can happen when changing directories
self.filename = None
return
objtype, sha1, name = self.model.subtree_node(idx)
curdir = self.model.directory
if curdir:
self.filename = os.path.join(curdir, name)
else:
self.filename = name
catguts = self.model.git.cat_file(objtype, sha1,
with_raw_output=True)
self.view.commit_text.setText(catguts)
self.view.revision.setText(sha1)
self.view.revision.selectAll()
# Copy the sha1 into the clipboard
qtutils.set_clipboard(sha1)
# automatically called by qobserver
def commit_list_doubleclick(self,*rest):
"""
Called when an entry is double-clicked.
This callback changes the model's directory when
invoked on a directory item. When invoked on a file
it allows the file to be saved.
"""
current = self.view.commit_list.currentRow()
directories = self.model.directories
# A file item was double-clicked.
# Create a save-as dialog and export the file,
# or if in get_file mode, grab the filename and finish the dialog.
if current >= len(directories):
idx = current - len(directories)
objtype, sha1, name = self.model.subtree_node(idx)
if self.get_file:
if self.model.directory:
curdir = self.model.directory
self.filename = os.path.join(curdir, name)
else:
self.filename = name
self.view.accept()
return
nameguess = os.path.join(self.model.directory, name)
filename = qtutils.save_dialog(self.view, 'Save', nameguess)
if not filename:
return
self.model.set_directory(os.path.dirname(filename))
contents = self.model.git.cat_file(objtype, sha1,
with_raw_output=True)
utils.write(filename, contents)
return
dirent = directories[current]
curdir = self.model.directory
# "change directories"
# '..' is a special case--it doesn't really exist...
if dirent == '..':
newdir = os.path.dirname(os.path.dirname(curdir))
if newdir == '':
self.model.set_directory(newdir)
else:
self.model.set_directory(newdir + os.sep)
else:
self.model.set_directory(curdir + dirent)
def _display_items(self):
"""
Populate the commit_list with the current directories and items
Directories are always listed first.
"""
# Clear commit/revision fields
self.view.commit_text.setText('')
self.view.revision.setText('')
dir_icon = resources.icon('dir.png')
file_icon = resources.icon('generic.png')
# Factory method for creating items
creator = qtutils.create_listwidget_item
# First the directories,
qtutils.set_items(self.view.commit_list,
map(lambda d: creator(d, dir_icon),
self.model.directories))
# and now the filenames
qtutils.add_items(self.view.commit_list,
map(lambda s: creator(s, file_icon),
self.model.subtree_names))
+24 -28
View File
@@ -4,9 +4,13 @@
import os
import re
import time
import shlex
import subprocess
from PyQt4 import QtGui
import cola
from cola import gitcmds
from cola import qtutils
from cola.qobserver import QObserver
from cola.models.search import SearchModel
@@ -24,23 +28,22 @@ COMMITTER = 'radio_committer'
DATE_RANGE = 'radio_daterange'
def search(searchtype, browse=False):
def search():
"""Return a callback to handle various search actions."""
def search_handler():
search_commits(cola.model(),
QtGui.QApplication.instance().activeWindow(),
searchtype,
browse)
return search_handler
search_commits(cola.model(),
QtGui.QApplication.instance().activeWindow())
class SearchEngine(object):
def __init__(self, model):
self.model = model
def rev_args(self):
max = self.model.max_results
return { 'max-count': max, 'pretty': 'format:%H %aN - %s - %ar' }
max_count = self.model.max_results
return {
'no_color': True,
'max-count': max_count,
'pretty': 'format:%H %aN - %s - %ar',
}
def common_args(self):
return (self.model.query, self.rev_args())
@@ -55,7 +58,7 @@ class SearchEngine(object):
def revisions(self, *args, **kwargs):
revlist = self.model.git.log(*args, **kwargs)
return self.model.parse_rev_list(revlist)
return gitcmds.parse_rev_list(revlist)
def results(self):
pass
@@ -80,7 +83,10 @@ class RevisionRangeSearch(SearchEngine):
class PathSearch(SearchEngine):
def results(self):
query, args = self.common_args()
paths = ['--'] + query.split(':')
try:
paths = ['--'] + shlex.split(str(query))
except UnicodeEncodeError:
paths = ['--'] + query.split(' ')
return self.revisions(all=True, *paths, **args)
class MessageSearch(SearchEngine):
@@ -101,7 +107,7 @@ class CommitterSearch(SearchEngine):
class DiffSearch(SearchEngine):
def results(self):
query, kwargs = self.common_args()
return self.model.parse_rev_list(
return gitcmds.parse_rev_list(
self.model.git.log('-S'+query, all=True, **kwargs))
class DateRangeSearch(SearchEngine):
@@ -157,14 +163,6 @@ class SearchController(QObserver):
radio_committer = self.search_callback,
radio_daterange = self.search_callback,
)
self.update_fonts()
def update_fonts(self):
font = self.model.cola_config('fontdiff')
if font:
qfont = QtGui.QFont()
qfont.fromString(font)
self.view.commit_text.setFont(qfont)
def set_mode(self, mode):
radio = getattr(self.view, mode)
@@ -202,7 +200,7 @@ class SearchController(QObserver):
if not path.startswith(os.getcwd()):
continue
filepaths.append(path[lenprefix:])
query = ':'.join(filepaths)
query = subprocess.list2cmdline(filepaths)
self.model.set_query('')
self.set_mode(PATH)
self.model.set_query(query)
@@ -219,7 +217,7 @@ class SearchController(QObserver):
return
revision = self.results[row][0]
qtutils.set_clipboard(revision)
diff = self.model.commit_diff(revision)
diff = gitcmds.commit_diff(revision)
self.view.commit_text.setText(diff)
def export_patch(self):
@@ -240,7 +238,7 @@ class SearchController(QObserver):
with_stderr=True,
with_status=True))
def search_commits(model, parent, mode, browse):
def search_commits(model, parent):
def date(timespec):
return '%04d-%02d-%02d' % time.localtime(timespec)[:3]
@@ -248,9 +246,7 @@ def search_commits(model, parent, mode, browse):
model = SearchModel(cwd=model.git.worktree())
view = SearchView(parent)
ctl = SearchController(model, view)
ctl.set_mode(mode)
model.set_start_date(date(time.time()-(87640*7)))
ctl.set_mode(MESSAGE)
model.set_start_date(date(time.time()-(87640*31)))
model.set_end_date(date(time.time()+87640))
view.show()
if browse:
ctl.browse_callback()
+8 -14
View File
@@ -2,9 +2,13 @@
"""
from PyQt4 import QtGui
from PyQt4.QtCore import SIGNAL
import cola
from cola import gitcmds
from cola import qtutils
from cola import serializer
from cola.prefs import diff_font
from cola.views.selectcommits import SelectCommitsView
from cola.qobserver import QObserver
from cola.controllers.createbranch import create_new_branch
@@ -24,26 +28,16 @@ class SelectCommitsController(QObserver):
def __init__(self, model, view, revs, summaries):
self.orig_model = model
QObserver.__init__(self, model.clone(), view)
QObserver.__init__(self, serializer.clone(model), view)
self.model.set_revisions(revs)
self.model.set_summaries(summaries)
self.connect(view.commit_list,
'itemSelectionChanged()',
SIGNAL('itemSelectionChanged()'),
self.commit_sha1_selected)
view.commit_list.contextMenuEvent = self.context_menu_event
self.set_diff_font()
def set_diff_font(self):
if not self.model.has_param('global_cola_fontdiff'):
return
font = self.model.param('global_cola_fontdiff')
if not font:
return
qf = QtGui.QFont()
qf.fromString(font)
self.view.commit_text.setFont(qf)
self.view.commit_text.setFont(diff_font())
def select_commits(self):
summaries = self.model.summaries
@@ -103,7 +97,7 @@ class SelectCommitsController(QObserver):
self.view.revision.selectAll()
# Lookup the sha1's commit
commit_diff = self.model.commit_diff(sha1)
commit_diff = gitcmds.commit_diff(sha1)
self.view.commit_text.setText(commit_diff)
# Copy the sha1 into the clipboard
-146
View File
@@ -1,146 +0,0 @@
"""This controller handles the stash dialog."""
import os
from PyQt4 import QtGui
import cola
from cola import utils
from cola import qtutils
from cola import signals
from cola.qobserver import QObserver
from cola.views import stash as stashmod
def stash():
"""Launches a stash dialog using the provided model + view
"""
model = cola.model()
model.keep_index = True
model.stash_list = []
model.stash_revids = []
parent = QtGui.QApplication.instance().activeWindow()
view = stashmod.StashView(parent)
ctl = StashController(model, view)
view.show()
class StashController(QObserver):
"""The StashController is the brains behind the 'Stash' dialog
"""
def __init__(self, model, view):
QObserver.__init__(self, model, view)
self.add_observables('stash_list', 'keep_index')
self.add_callbacks(button_stash_show = self.stash_show,
button_stash_apply = self.stash_apply,
button_stash_drop = self.stash_drop,
button_stash_clear = self.stash_clear,
button_stash_save = self.stash_save)
self.update_model()
def update_model(self):
"""Initiates git queries on the model and updates the view
"""
self.model.set_stash_list(self.model.parse_stash_list())
self.model.set_stash_revids(self.model.parse_stash_list(revids=True))
self.refresh_view()
def selected_stash(self):
"""Returns the stash name of the currently selected stash
"""
list_widget = self.view.stash_list
stash_list = self.model.stash_revids
return qtutils.selected_item(list_widget, stash_list)
def stash_save(self):
"""Saves the worktree in a stash
This prompts the user for a stash name and creates
a git stash named accordingly.
"""
if not qtutils.question(self.view,
'Stash Changes?',
'This will stash your current '
'changes away for later use.\n'
'Continue?'):
return
stash_name, ok = qtutils.prompt('Enter a name for this stash')
if not ok:
return
while stash_name in self.model.stash_list:
qtutils.information('Oops!',
'That name already exists. '
'Please enter another name.')
stash_name, ok = qtutils.prompt('Enter a name for this stash')
if not ok:
return
if not stash_name:
return
# Sanitize the stash name
stash_name = utils.sanitize(stash_name)
args = []
if self.model.keep_index:
args.append('--keep-index')
args.append(stash_name)
qtutils.log(*self.model.git.stash('save',
with_stderr=True,
with_status=True,
*args))
self.view.accept()
def stash_show(self):
"""Shows the current stash in the main view."""
selection = self.selected_stash()
if not selection:
return
diffstat = self.model.git.stash('show', selection)
diff = self.model.git.stash('show', '-p', selection)
cola.notifier().broadcast(signals.diff_text, '%s\n\n%s' % (diffstat, diff))
def stash_apply(self):
"""Applies the currently selected stash
"""
selection = self.selected_stash()
if not selection:
return
qtutils.log(*self.model.git.stash('apply', '--index', selection,
with_stderr=True,
with_status=True))
self.view.accept()
def stash_drop(self):
"""Drops the currently selected stash
"""
selection = self.selected_stash()
if not selection:
return
if not qtutils.question(self.view,
'Drop Stash?',
'This will permanently remove the '
'selected stash.\n'
'Recovering these changes may not '
'be possible.\n\n'
'Continue?'):
return
qtutils.log(*self.model.git.stash('drop', selection,
with_stderr=True,
with_status=True))
self.update_model()
def stash_clear(self):
"""Clears all stashes
"""
if not qtutils.question(self.view,
'Drop All Stashes?',
'This will permanently remove '
'ALL stashed changes.\n'
'Recovering these changes may not '
'be possible.\n\n'
'Continue?'):
return
self.model.git.stash('clear'),
self.update_model()
-25
View File
@@ -1,25 +0,0 @@
"""This module provides utility controllers.
"""
from PyQt4 import QtGui
from cola import qtutils
from cola.views import itemlist
from cola.views import combo
from cola.qobserver import QObserver
def choose_from_combo(title, items):
"""Quickly choose an item from a list using a combo box"""
parent = QtGui.QApplication.instance().activeWindow()
return combo.ComboView(parent,
title=title,
items=items).selected()
def choose_from_list(title, items=None, dblclick=None):
"""Quickly choose an item from a list using a list widget"""
parent = QtGui.QApplication.instance().activeWindow()
return itemlist.ListView(parent,
title=title,
items=items,
dblclick=dblclick).selected()
+34
View File
@@ -0,0 +1,34 @@
from PyQt4.QtCore import SIGNAL
from cola.qtutils import SLOT
from cola.cmdfactory import CommandFactory
class Controller(object):
def __init__(self, model, view):
self.model = model
self.view = view
self.commands = {}
self.factory = CommandFactory(model)
def add_command_directory(self, command_directory):
for signal, cmdclass in command_directory.items():
self.factory.add_command(signal, cmdclass)
def add_command(self, signal, handler=None):
if handler is None:
handler = self.do_wrapper(signal)
self.view.connect(self.view, SIGNAL(signal), handler)
def add_commands(self, command_directory, handler=None):
for signal, cmdclass in command_directory.items():
self.factory.add_command(signal, cmdclass)
self.add_command(signal, handler=handler)
def add_global_command(self, signal):
self.view.connect(self.view, SIGNAL(signal), SLOT(signal))
def do_wrapper(self, signal):
def do(*args, **opts):
return self.factory.do(signal, *args, **opts)
return do
+41
View File
@@ -0,0 +1,41 @@
import os
import sys
from PyQt4 import QtCore
if __name__ == '__main__':
# Find the source tree
src = os.path.join(os.path.dirname(__file__), '..', '..')
sys.path.insert(1, os.path.join(os.path.abspath(src), 'thirdparty'))
sys.path.insert(1, os.path.abspath(src))
from cola.dag.view import DAGView
from cola.dag.model import DAG
from cola.dag.controller import DAGController
def git_dag(model, parent, opts=None, args=None):
"""Return a pre-populated git DAG widget."""
dag = DAG(model.currentbranch, 1000)
dag.set_options(opts, args)
view = DAGView(model, dag, parent=parent)
ctl = DAGController(dag, view)
view.show()
view.raise_()
if dag.ref:
view.thread.start(QtCore.QThread.LowPriority)
return ctl
if __name__ == "__main__":
import cola
from cola import app
model = cola.model()
model.use_worktree(os.getcwd())
model.update_status()
app = app.ColaApplication(sys.argv)
ctl = git_dag(model, app.activeWindow())
sys.exit(app.exec_())
+8
View File
@@ -0,0 +1,8 @@
from cola.ctrl import Controller
from cola.dag.model import command_directory
class DAGController(Controller):
def __init__(self, model, view):
Controller.__init__(self, model, view)
self.add_commands(command_directory)
+264
View File
@@ -0,0 +1,264 @@
import subprocess
import cola
from cola import core
from cola import git
from cola import signals
from cola import utils
from cola.cmds import BaseCommand
from cola.observable import Observable
# put subject at the end b/c it can contain
# any number of funky characters
logfmt = 'format:%H%x01%P%x01%d%x01%an%x01%aD%x01%s'
git = git.instance()
archive = 'archive'
class CommitFactory(object):
root_generation = 0
_commits = {}
@classmethod
def reset(cls):
cls._commits.clear()
cls.root_generation = 0
@classmethod
def new(cls, sha1=None, log_entry=None):
if not sha1 and log_entry:
sha1 = log_entry[:40]
try:
commit = cls._commits[sha1]
if log_entry and not commit.parsed:
commit.parse(log_entry)
cls.root_generation = max(commit.generation,
cls.root_generation)
except KeyError:
commit = Commit(sha1=sha1,
log_entry=log_entry)
if not log_entry:
cls.root_generation += 1
commit.generation = max(commit.generation,
cls.root_generation)
cls._commits[sha1] = commit
return commit
class DAG(Observable):
ref_updated = 'ref_updated'
count_updated = 'count_updated'
def __init__(self, ref, count):
Observable.__init__(self)
self.ref = ref
self.count = count
self.overrides = {}
def set_ref(self, ref):
changed = ref != self.ref
self.ref = ref
self.notify_message_observers(self.ref_updated)
return changed
def set_count(self, count):
changed = count != self.count
self.count = count
self.notify_message_observers(self.count_updated)
return changed
def set_options(self, opts, args):
if opts is not None:
if self.set_count(opts.count):
self.overrides['count'] = opts.count
if args is not None:
ref = subprocess.list2cmdline([core.decode(a) for a in args])
if self.set_ref(ref):
self.overrides['ref'] = ref
def overridden(self, opt):
return opt in self.overrides
class Commit(object):
root_generation = 0
__slots__ = ('sha1',
'subject',
'parents',
'children',
'tags',
'author',
'authdate',
'generation',
'parsed')
def __init__(self, sha1=None, log_entry=None):
self.sha1 = sha1
self.subject = None
self.parents = []
self.children = []
self.tags = set()
self.author = None
self.authdate = None
self.parsed = False
self.generation = CommitFactory.root_generation
if log_entry:
self.parse(log_entry)
def parse(self, log_entry):
self.sha1 = log_entry[:40]
(parents, tags, author, authdate, subject) = \
log_entry[41:].split(chr(0x01), 5)
if subject:
self.subject = core.decode(subject)
if parents:
generation = None
for parent_sha1 in parents.split(' '):
parent = CommitFactory.new(sha1=parent_sha1)
parent.children.append(self)
if generation is None:
generation = parent.generation+1
self.parents.append(parent)
generation = max(parent.generation+1, generation)
self.generation = generation
if tags:
for tag in tags[2:-1].split(', '):
if tag.startswith('tag: '):
tag = tag[5:] # tag: refs/
elif tag.startswith('refs/remotes/'):
tag = tag[13:] # refs/remotes/
elif tag.startswith('refs/heads/'):
tag = tag[11:] # refs/heads/
if tag.endswith('/HEAD'):
continue
self.tags.add(core.decode(tag))
if author:
self.author = core.decode(author)
if authdate:
self.authdate = authdate
self.parsed = True
return self
def __str__(self):
return self.sha1
def __repr__(self):
return ("{\n"
" sha1: " + self.sha1 + "\n"
" subject: " + self.subject + "\n"
" author: " + self.author + "\n"
" authdate: " + self.authdate + "\n"
" parents: [" + ', '.join(self.parents) + "]\n"
" tags: [" + ', '.join(self.tags) + "]\n"
"}")
class RepoReader(object):
def __init__(self, dag, git=git):
self.dag = dag
self.git = git
self._proc = None
self._objects = {}
self._cmd = ['git', 'log',
'--topo-order',
'--reverse',
'--pretty='+logfmt]
self._cached = False
"""Indicates that all data has been read"""
self._idx = -1
"""Index into the cached commits"""
self._topo_list = []
"""List of commits objects in topological order"""
cached = property(lambda self: self._cached)
"""Return True when no commits remain to be read"""
def __len__(self):
return len(self._topo_list)
def reset(self):
CommitFactory.reset()
if self._proc:
self._topo_list = []
self._proc.kill()
self._proc = None
self._cached = False
def __iter__(self):
if self._cached:
return self
self.reset()
return self
def next(self):
if self._cached:
try:
self._idx += 1
return self._topo_list[self._idx]
except IndexError:
self._idx = -1
raise StopIteration
if self._proc is None:
ref_args = utils.shell_split(self.dag.ref)
cmd = self._cmd + ['-%d' % self.dag.count] + ref_args
self._proc = utils.start_command(cmd)
self._topo_list = []
log_entry = self._proc.stdout.readline().rstrip()
if not log_entry:
del self._proc
self._cached = True
self._proc = None
raise StopIteration
sha1 = log_entry[:40]
try:
return self._objects[sha1]
except KeyError:
c = CommitFactory.new(log_entry=log_entry)
self._objects[c.sha1] = c
self._topo_list.append(c)
return c
def __getitem__(self, sha1):
return self._objects[sha1]
def items(self):
return self._objects.items()
class Archive(BaseCommand):
def __init__(self, ref, fmt, prefix, filename):
BaseCommand.__init__(self)
self.ref = ref
self.fmt = fmt
self.prefix = prefix
self.filename = filename
def do(self):
fp = open(core.encode(self.filename), 'wb')
cmd = ['git', 'archive', '--format='+self.fmt]
if self.fmt in ('tgz', 'tar.gz'):
cmd.append('-9')
if self.prefix:
cmd.append('--prefix=' + self.prefix)
cmd.append(self.ref)
proc = utils.start_command(cmd, stdout=fp)
out, err = proc.communicate()
fp.close()
if not out:
out = ''
if err:
out += err
status = proc.returncode
cola.notifier().broadcast(signals.log_cmd, status, out)
command_directory = {
archive: Archive,
}
+1347
View File
File diff suppressed because it is too large Load Diff
+53
View File
@@ -0,0 +1,53 @@
__all__ = ('decorator', 'deprecated', 'memoize')
def decorator(caller, func=None):
"""
decorator(caller) converts a caller function into a decorator;
decorator(caller, func) decorates a function using a caller.
"""
if func is None: # returns a decorator
def _decorator(f, *args, **opts):
def _caller(*args, **opts):
return caller(f, *args, **opts)
return _caller
return _decorator
else: # returns a decorated function
def _decorator(*args, **opts):
return caller(func, *args, **opts)
return _decorator
@decorator
def deprecated(func, *args, **kw):
"A decorator for deprecated functions"
import warnings
warnings.warn('Calling deprecated function %r' % func.__name__,
DeprecationWarning, stacklevel=3)
return func(*args, **kw)
def memoize(func):
"""
A decorator for memoizing function calls
http://en.wikipedia.org/wiki/Memoization
"""
func.cache = {}
return decorator(_memoize, func)
def _memoize(func, *args, **opts):
"""Implements memoized cache lookups"""
if opts: # frozenset is used to ensure hashability
key = args, frozenset(opts.items())
else:
key = args
cache = func.cache # attribute added by memoize
try:
return cache[key]
except KeyError:
cache[key] = result = func(*args, **opts)
return result
+42 -27
View File
@@ -1,7 +1,8 @@
import os
import re
from cola.utils import write
from cola import utils
from cola import gitcmds
class DiffParser(object):
@@ -23,11 +24,11 @@ class DiffParser(object):
self.diffs = []
self.selected = []
(header, diff) = model.diff_helper(filename=filename,
branch=branch,
with_diff_header=True,
cached=cached and not bool(branch),
reverse=cached or bool(branch) or reverse)
(header, diff) = gitcmds.diff_helper(filename=filename,
branch=branch,
with_diff_header=True,
cached=cached and not bool(branch),
reverse=cached or bool(branch) or reverse)
self.model = model
self.diff = diff
self.header = header
@@ -35,17 +36,17 @@ class DiffParser(object):
# Always index into the non-reversed diff
self.fwd_header, self.fwd_diff = \
model.diff_helper(filename=filename,
branch=branch,
with_diff_header=True,
cached=cached and not bool(branch),
reverse=bool(branch))
gitcmds.diff_helper(filename=filename,
branch=branch,
with_diff_header=True,
cached=cached and not bool(branch),
reverse=bool(branch))
def write_diff(self,filename,which,selected=False,noop=False):
"""Writes a new diff corresponding to the user's selection."""
if not noop and which < len(self.diffs):
diff = self.diffs[which]
write(filename, self.header + '\n' + diff + '\n')
utils.write(filename, self.header + '\n' + diff + '\n')
return True
else:
return False
@@ -205,32 +206,46 @@ class DiffParser(object):
if special_selection in self.fwd_diff:
selection = special_selection
else:
return
return 0, ''
start = self.fwd_diff.index(selection)
end = start + len(selection)
self.set_diffs_to_range(start, end)
else:
self.set_diff_to_offset(offset)
selected = False
output = ''
status = 0
# Process diff selection only
if selected:
for idx in self.selected:
contents = self.diff_subset(idx, start, end)
if contents:
tmpfile = self.model.tmp_filename()
write(tmpfile, contents)
if apply_to_worktree:
self.model.apply_diff_to_worktree(tmpfile)
else:
self.model.apply_diff(tmpfile)
os.unlink(tmpfile)
if not contents:
continue
tmpfile = self.model.tmp_filename()
utils.write(tmpfile, contents)
if apply_to_worktree:
stat, out = self.model.apply_diff_to_worktree(tmpfile)
output += out
status = max(status, stat)
else:
stat, out = self.model.apply_diff(tmpfile)
output += out
status = max(status, stat)
os.unlink(tmpfile)
# Process a complete hunk
else:
for idx, diff in enumerate(self.diffs):
tmpfile = self.model.tmp_filename()
if self.write_diff(tmpfile,idx):
if apply_to_worktree:
self.model.apply_diff_to_worktree(tmpfile)
else:
self.model.apply_diff(tmpfile)
os.unlink(tmpfile)
if not self.write_diff(tmpfile,idx):
continue
if apply_to_worktree:
stat, out = self.model.apply_diff_to_worktree(tmpfile)
output += out
status = max(status, stat)
else:
stat, out = self.model.apply_diff(tmpfile)
output += out
status = max(status, stat)
os.unlink(tmpfile)
return status, output
+106
View File
@@ -1,7 +1,113 @@
from PyQt4 import QtGui
from PyQt4 import QtCore
from PyQt4.QtCore import SIGNAL
from cola import utils
from cola import qtutils
from cola import gitcmds
def launch(args):
"""Launches 'git difftool' with args"""
difftool_args = ['git', 'difftool', '--no-prompt']
difftool_args.extend(args)
utils.fork(difftool_args)
def diff_commits(parent, a, b):
dlg = FileDiffDialog(parent, a, b)
dlg.show()
dlg.raise_()
return dlg.exec_() == QtGui.QDialog.Accepted
class FileDiffDialog(QtGui.QDialog):
def __init__(self, parent, a, b):
QtGui.QDialog.__init__(self, parent)
self.a = a
self.b = b
self.setWindowTitle('Select File(s)')
self._tree = QtGui.QTreeWidget(self)
self._tree.setAlternatingRowColors(True)
self._tree.setRootIsDecorated(False)
self._tree.setSelectionMode(self._tree.ExtendedSelection)
self._tree.setUniformRowHeights(True)
self._tree.setAllColumnsShowFocus(True)
self._tree.setHeaderHidden(True)
self._diff_btn = QtGui.QPushButton('Compare')
self._diff_btn.setIcon(qtutils.ok_icon())
self._diff_btn.setEnabled(False)
self._close_btn = QtGui.QPushButton('Close')
self._close_btn.setIcon(qtutils.close_icon())
self._button_layt = QtGui.QHBoxLayout()
self._button_layt.setMargin(0)
self._button_layt.addStretch()
self._button_layt.addWidget(self._diff_btn)
self._button_layt.addWidget(self._close_btn)
self._layt = QtGui.QVBoxLayout()
self._layt.setMargin(4)
self._layt.addWidget(self._tree)
self._layt.addLayout(self._button_layt)
self.setLayout(self._layt)
qtutils.add_close_action(self)
self.connect(self._tree, SIGNAL('itemSelectionChanged()'),
self._tree_selection_changed)
self.connect(self._tree,
SIGNAL('itemDoubleClicked(QTreeWidgetItem*,int)'),
self._tree_double_clicked)
self.connect(self._diff_btn, SIGNAL('clicked()'), self.diff)
self.connect(self._close_btn, SIGNAL('clicked()'), self.close)
self.diff_arg = '%s..%s' % (self.a, self.b)
self.resize(720, 420)
def exec_(self):
filenames = gitcmds.diff_filenames(self.diff_arg)
if not filenames:
details = ('"git diff --name-only %s" returned an empty list' %
self.diff_arg)
self.hide()
qtutils.information('git cola',
message='No changes to diff',
details=details,
parent=self)
self.close()
return self.Accepted
icon = qtutils.file_icon()
items = []
for filename in filenames:
item = QtGui.QTreeWidgetItem()
item.setIcon(0, icon)
item.setText(0, filename)
item.setData(0, QtCore.Qt.UserRole, QtCore.QVariant(filename))
items.append(item)
self._tree.addTopLevelItems(items)
return QtGui.QDialog.exec_(self)
def _tree_selection_changed(self):
self._diff_btn.setEnabled(bool(self._tree.selectedItems()))
def _tree_double_clicked(self, item, column):
path = item.data(0, QtCore.Qt.UserRole).toPyObject()
launch([self.diff_arg, '--', unicode(path)])
def diff(self):
items = self._tree.selectedItems()
if not items:
return
paths = [i.data(0, QtCore.Qt.UserRole).toPyObject() for i in items]
for path in paths:
launch([self.diff_arg, '--', unicode(path)])
+9
View File
@@ -3,11 +3,20 @@ class ColaError(StandardError):
"""The base class of all cola exceptions"""
pass
class GitCommandError(ColaError):
"""Exception class for failed commands."""
pass
class GitInitError(ColaError):
"""Exception class for errors related to just-initialized repositories."""
pass
class UsageError(ColaError):
"""Exception class for usage errors."""
def __init__(self, title, message):
ColaError.__init__(self, message)
self.title = title
self.message = message
+103 -54
View File
@@ -4,35 +4,27 @@
# This module is part of GitPython and is released under
# the BSD License: http://www.opensource.org/licenses/bsd-license.php
import re
import os
import sys
import errno
import subprocess
import threading
cmdlock = threading.Lock()
import cola
from cola import core
from cola import errors
from cola import signals
from cola.decorators import memoize
cmdlock = threading.Lock()
# Enables debugging of GitPython's git commands
GIT_PYTHON_TRACE = os.getenv('GIT_PYTHON_TRACE', False)
GIT_COLA_TRACE = False
def dashify(string):
return string.replace('_', '-')
# Enables debugging of GitPython's git commands
GIT_PYTHON_TRACE = os.environ.get("GIT_PYTHON_TRACE", False)
execute_kwargs = ('cwd',
'istream',
'with_exceptions',
'with_raw_output',
'with_status',
'with_stderr')
extra = {}
if sys.platform == 'win32':
extra = {'shell': True}
class Git(object):
"""
@@ -40,6 +32,77 @@ class Git(object):
"""
def __init__(self):
self._git_cwd = None #: The working directory used by execute()
self.load_worktree(os.getcwd())
def load_worktree(self, path):
self._git_dir = path
self._worktree = None
self.worktree()
def worktree(self):
if self._worktree:
return self._worktree
self.git_dir()
if self._git_dir:
curdir = self._git_dir
else:
curdir = os.getcwd()
if self._is_git_dir(os.path.join(curdir, '.git')):
return curdir
# Handle bare repositories
if (len(os.path.basename(curdir)) > 4
and curdir.endswith('.git')):
return curdir
if 'GIT_WORK_TREE' in os.environ:
self._worktree = os.getenv('GIT_WORK_TREE')
if not self._worktree or not os.path.isdir(self._worktree):
if self._git_dir:
gitparent = os.path.join(os.path.abspath(self._git_dir), '..')
self._worktree = os.path.abspath(gitparent)
self.set_cwd(self._worktree)
return self._worktree
def is_valid(self):
return self._git_dir and self._is_git_dir(self._git_dir)
def git_path(self, *paths):
return os.path.join(self.git_dir(), *paths)
def git_dir(self):
if self.is_valid():
return self._git_dir
if 'GIT_DIR' in os.environ:
self._git_dir = os.getenv('GIT_DIR')
if self._git_dir:
curpath = os.path.abspath(self._git_dir)
else:
curpath = os.path.abspath(os.getcwd())
# Search for a .git directory
while curpath:
if self._is_git_dir(curpath):
self._git_dir = curpath
break
gitpath = os.path.join(curpath, '.git')
if self._is_git_dir(gitpath):
self._git_dir = gitpath
break
curpath, dummy = os.path.split(curpath)
if not dummy:
break
return self._git_dir
def _is_git_dir(self, d):
"""From git's setup.c:is_git_directory()."""
if (os.path.isdir(d)
and os.path.isdir(os.path.join(d, 'objects'))
and os.path.isdir(os.path.join(d, 'refs'))):
headref = os.path.join(d, 'HEAD')
return (os.path.isfile(headref)
or (os.path.islink(headref)
and os.readlink(headref).startswith('refs')))
return False
def set_cwd(self, path):
"""Sets the current directory."""
@@ -102,8 +165,10 @@ class Git(object):
else:
stderr = None
extra = {}
if sys.platform == 'win32':
command = map(replace_carot, command)
extra = {'shell': True}
# Start the process
# Guard against thread-unsafe .git/index.lock files
@@ -119,7 +184,7 @@ class Git(object):
break
except OSError, e:
# Some systems interrupt system calls and throw OSError
if e.errno == errno.EINTR:
if e.errno in (errno.EINTR, errno.ENOMEM):
continue
cmdlock.release()
raise e
@@ -148,6 +213,10 @@ class Git(object):
else:
print "%s -> %d" % (command, status)
if GIT_COLA_TRACE:
msg = 'trace: ' + subprocess.list2cmdline(command)
cola.notifier().broadcast(signals.log_cmd, status, msg)
# Allow access to the command's status code
if with_status:
return (status, output)
@@ -193,11 +262,18 @@ class Git(object):
Returns
Same as execute()
"""
# Handle optional arguments prior to calling transform_kwargs
# otherwise they'll end up in args, which is bad.
_kwargs = dict(cwd=self._git_cwd)
execute_kwargs = ('cwd', 'istream',
'with_exceptions',
'with_raw_output',
'with_status',
'with_stderr')
for kwarg in execute_kwargs:
if kwarg in kwargs:
_kwargs[kwarg] = kwargs.pop(kwarg)
@@ -227,45 +303,18 @@ def replace_carot(cmd_arg):
return cmd_arg.replace('^', '^^')
def shell_quote(*strings):
"""
Quote strings so that they can be suitably martialled
off to the shell. This method supports POSIX sh syntax.
This is crucial to properly handle command line arguments
with spaces, quotes, double-quotes, etc. on darwin/win32...
"""
@memoize
def instance():
"""Return the Git singleton"""
return Git()
regex = re.compile('[^\w!%+,\-./:@^]')
quote_regex = re.compile("((?:'\\''){2,})")
ret = []
for s in strings:
if not s:
continue
git = instance()
"""
Git command singleton
if '\x00' in s:
raise ValueError('No way to quote strings '
'containing null(\\000) bytes')
>>> from cola.git import git
>>> 'git' == git.version()[:3]
True
# = does need quoting else in command position it's a
# program-local environment setting
match = regex.search(s)
if match and '=' not in s:
# ' -> '\''
s = s.replace("'", "'\\''")
# make multiple ' in a row look simpler
# '\'''\'''\'' -> '"'''"'
quote_match = quote_regex.match(s)
if quote_match:
quotes = match.group(1)
s.replace(quotes, ("'" *(len(quotes)/4)) + "\"'")
s = "'%s'" % s
if s.startswith("''"):
s = s[2:]
if s.endswith("''"):
s = s[:-2]
ret.append(s)
return ' '.join(ret)
"""
+229
View File
@@ -0,0 +1,229 @@
import os
import copy
import fnmatch
from cola import core
from cola import git
from cola import observable
from cola.decorators import memoize
@memoize
def instance():
"""Return a static GitConfig instance."""
return GitConfig()
def _stat_info():
# Try /etc/gitconfig as a fallback for the system config
userconfig = os.path.expanduser(os.path.join('~', '.gitconfig'))
paths = (('system', '/etc/gitconfig'),
('user', core.decode(userconfig)),
('repo', core.decode(git.instance().git_path('config'))))
statinfo = []
for category, path in paths:
try:
statinfo.append((category, path, os.stat(path).st_mtime))
except OSError:
continue
return statinfo
def _cache_key():
# Try /etc/gitconfig as a fallback for the system config
userconfig = os.path.expanduser(os.path.join('~', '.gitconfig'))
paths = ('/etc/gitconfig',
userconfig,
git.instance().git_path('config'))
mtimes = []
for path in paths:
try:
mtimes.append(os.stat(path).st_mtime)
except OSError:
continue
return mtimes
class GitConfig(observable.Observable):
"""Encapsulate access to git-config values."""
message_user_config_changed = 'user_config_changed'
message_repo_config_changed = 'repo_config_changed'
def __init__(self):
observable.Observable.__init__(self)
self.git = git.instance()
self._system = {}
self._user = {}
self._repo = {}
self._all = {}
self._cache_key = None
self._configs = []
self._config_files = {}
self._find_config_files()
def reset(self):
self._system = {}
self._user = {}
self._repo = {}
self._all = {}
self._cache_key = None
self._configs = []
self._config_files = {}
self._find_config_files()
def user(self):
return copy.deepcopy(self._user)
def repo(self):
return copy.deepcopy(self._repo)
def all(self):
return copy.deepcopy(self._all)
def _find_config_files(self):
"""
Classify git config files into 'system', 'user', and 'repo'.
Populates self._configs with a list of the files in
reverse-precedence order. self._config_files is populated with
{category: path} where category is one of 'system', 'user', or 'repo'.
"""
# Try the git config in git's installation prefix
statinfo = _stat_info()
self._configs = map(lambda x: x[1], statinfo)
self._config_files = {}
for (cat, path, mtime) in statinfo:
self._config_files[cat] = path
def update(self):
"""Read config values from git."""
if self._cached():
return
self._read_configs()
def _cached(self):
"""
Return True when the cache matches.
Updates the cache and returns False when the cache does not match.
"""
cache_key = _cache_key()
if not self._cache_key or cache_key != self._cache_key:
self._cache_key = cache_key
return False
return True
def _read_configs(self):
"""Read git config value into the system, user and repo dicts."""
self._system = {}
self._user = {}
self._repo = {}
self._all = {}
if 'system' in self._config_files:
self._system = self.read_config(self._config_files['system'])
if 'user' in self._config_files:
self._user = self.read_config(self._config_files['user'])
if 'repo' in self._config_files:
self._repo = self.read_config(self._config_files['repo'])
self._all = {}
for dct in (self._system, self._user, self._repo):
self._all.update(dct)
def read_config(self, path):
"""Return git config data from a path as a dictionary."""
dest = {}
args = ('--null', '--file', path, '--list')
config_lines = self.git.config(*args).split('\0')
for line in config_lines:
try:
k, v = line.split('\n')
except:
# the user has an invalid entry in their git config
continue
v = core.decode(v)
if v == 'yes':
v = 'true'
elif v == 'no':
v = 'false'
if v == 'true' or v == 'false':
v = (v == 'true')
try:
v = int(eval(v))
except:
pass
dest[k.lower()] = v
return dest
def get(self, key, default=None, source=None):
"""Return the string value for a config key."""
self.update()
return self._all.get(key, default)
def get_user(self, key, default=None):
self.update()
return self._user.get(key, default)
def get_repo(self, key, default=None):
self.update()
return self._repo.get(key, default)
def python_to_git(self, value):
if type(value) is bool:
if value:
return 'true'
else:
return 'false'
if type(value) is int:
return unicode(value)
return value
def set_user(self, key, value):
msg = self.message_user_config_changed
self.git.config('--global', key, self.python_to_git(value))
self.update()
self.notify_message_observers(msg, key, value)
def set_repo(self, key, value):
msg = self.message_repo_config_changed
self.git.config(key, self.python_to_git(value))
self.update()
self.notify_message_observers(msg, key, value)
def find(self, pat):
result = {}
for key, val in self._all.items():
if fnmatch.fnmatch(key, pat):
result[key] = val
return result
def get_encoding(self, default='utf-8'):
return self.get('gui.encoding', default=default)
guitool_opts = ('cmd', 'needsfile', 'noconsole', 'norescan', 'confirm',
'argprompt', 'revprompt', 'revunmerged', 'title', 'prompt')
def get_guitool_opts(self, name):
"""Return the guitool.<name> namespace as a dict"""
keyprefix = 'guitool.' + name + '.'
opts = {}
for cfg in self.guitool_opts:
value = self.get(keyprefix + cfg)
if value is None:
continue
opts[cfg] = value
return opts
def get_guitool_names(self):
cmds = []
guitools = self.find('guitool.*.cmd')
for name, cmd in guitools.items():
name = name[len('guitool.'):-len('.cmd')]
cmds.append(name)
return cmds
+628 -21
View File
@@ -1,30 +1,637 @@
"""Provides commands and queries for Git."""
import os
import re
from cStringIO import StringIO
import cola
from cola import core
from cola import git
from cola import gitcfg
from cola import errors
from cola import utils
from cola import version
from cola.compat import set
git = git.instance()
config = gitcfg.instance()
class InvalidRepositoryError(StandardError):
pass
def default_remote():
model = cola.model()
branch = model.currentbranch
branchconfig = 'branch.%s.remote' % branch
return model.local_config(branchconfig, 'origin')
def default_remote(config=None):
"""Return the remote tracked by the current branch."""
if config is None:
config = gitcfg.instance()
return config.get('branch.%s.remote' % current_branch())
def corresponding_remote_ref():
model = cola.model()
remote = default_remote()
branch = model.currentbranch
best_match = '%s/%s' % (remote, branch)
remote_branches = model.remote_branches
if not remote_branches:
return remote
for rb in remote_branches:
if rb == best_match:
return rb
return remote_branches[0]
def diff_filenames(arg):
def diff_filenames(arg, git=git):
"""Return a list of filenames that have been modified"""
model = cola.model()
diff_zstr = model.git.diff(arg, name_only=True, z=True).rstrip('\0')
diff_zstr = git.diff(arg, name_only=True, z=True,
**_common_diff_opts()).rstrip('\0')
return [core.decode(f) for f in diff_zstr.split('\0') if f]
def all_files(git=git):
"""Return the names of all files in the repository"""
return [core.decode(f)
for f in git.ls_files(z=True)
.strip('\0').split('\0') if f]
class _current_branch:
"""Cache for current_branch()"""
key = None
value = None
def clear_cache():
_current_branch.key = None
def current_branch(git=git):
"""Return the current branch"""
head = git.git_path('HEAD')
try:
key = os.stat(head).st_mtime
if _current_branch.key == key:
return _current_branch.value
except OSError, e:
pass
data = git.rev_parse('HEAD', with_stderr=True, symbolic_full_name=True)
if data.startswith('fatal:'):
# git init -- read .git/HEAD. We could do this unconditionally
# and avoid the subprocess call. It's probably time to start
# using dulwich.
data = _read_git_head(head)
for refs_prefix in ('refs/heads/', 'refs/remotes/'):
if data.startswith(refs_prefix):
value = core.decode(data[len(refs_prefix):])
_current_branch.key = key
_current_branch.value = value
return value
# Detached head
return data
def _read_git_head(head, default='master', git=git):
"""Pure-python .git/HEAD reader"""
# Legacy .git/HEAD symlinks
if os.path.islink(head):
refs_heads = os.path.realpath(git.git_path('refs', 'heads'))
path = os.path.abspath(head).replace('\\', '/')
if path.startswith(refs_heads + '/'):
return path[len(refs_heads)+1:]
# Common .git/HEAD "ref: refs/heads/master" file
elif os.path.isfile(head):
data = utils.slurp(core.decode(head)).rstrip()
ref_prefix = 'ref: '
if data.startswith(ref_prefix):
return data[len(ref_prefix):]
# Detached head
return data
return default
def branch_list(remote=False):
"""
Return a list of local or remote branches
This explicitly removes HEAD from the list of remote branches.
"""
if remote:
return for_each_ref_basename('refs/remotes')
else:
return for_each_ref_basename('refs/heads')
def for_each_ref_basename(refs, git=git):
"""Return refs starting with 'refs'."""
git_output = git.for_each_ref(refs, format='%(refname)')
output = core.decode(git_output).splitlines()
non_heads = filter(lambda x: not x.endswith('/HEAD'), output)
return map(lambda x: x[len(refs) + 1:], non_heads)
def all_refs(split=False, git=git):
"""Return a tuple of (local branches, remote branches, tags)."""
local_branches = []
remote_branches = []
tags = []
triple = lambda x, y: (x, len(x) + 1, y)
query = (triple('refs/tags', tags),
triple('refs/heads', local_branches),
triple('refs/remotes', remote_branches))
cmdout = core.decode(git.for_each_ref(format='%(refname)'))
for ref in cmdout.splitlines():
for prefix, prefix_len, dst in query:
if ref.startswith(prefix) and not ref.endswith('/HEAD'):
dst.append(ref[prefix_len:])
continue
if split:
return local_branches, remote_branches, tags
else:
return local_branches + remote_branches + tags
def tracked_branch(branch=None, config=None):
"""Return the remote branch associated with 'branch'."""
if config is None:
config = gitcfg.instance()
if branch is None:
branch = current_branch()
remote = config.get('branch.%s.remote' % branch)
if not remote:
return None
merge_ref = config.get('branch.%s.merge' % branch)
if not merge_ref:
return None
refs_heads = 'refs/heads/'
if merge_ref.startswith(refs_heads):
return remote + '/' + merge_ref[len(refs_heads):]
return None
def untracked_files(git=git):
"""Returns a sorted list of untracked files."""
ls_files = git.ls_files(z=True,
others=True,
exclude_standard=True)
return [core.decode(f) for f in ls_files.split('\0') if f]
def tag_list():
"""Return a list of tags."""
tags = for_each_ref_basename('refs/tags')
tags.reverse()
return tags
def commit_diff(sha1, git=git):
commit = git.show(sha1)
first_newline = commit.index('\n')
if commit[first_newline+1:].startswith('Merge:'):
return (core.decode(commit) + '\n\n' +
core.decode(diff_helper(commit=sha1,
cached=False,
suppress_header=False)))
else:
return core.decode(commit)
def _common_diff_opts(config=config):
submodule = version.check('diff-submodule', version.git_version())
return {
'patience': True,
'submodule': submodule,
'no_color': True,
'no_ext_diff': True,
'with_raw_output': True,
'with_stderr': True,
'unified': config.get('gui.diffcontext', 3),
}
def sha1_diff(sha1, git=git):
return core.decode(git.diff(sha1 + '^!', **_common_diff_opts()))
def diff_info(sha1, git=git, merge=True):
if merge:
mergefmt = 'Merge: %P%n'
else:
mergefmt = ''
log = git.log('-1',
'--pretty=format:'
'commit %H%n' +
mergefmt +
'Author: %aN <%aE>%n'
'Date: %aD%n'
'%n%s%n%n%b',
sha1)
return core.decode(log) + '\n\n' + sha1_diff(sha1)
def diff_helper(commit=None,
branch=None,
ref=None,
endref=None,
filename=None,
cached=True,
with_diff_header=False,
suppress_header=True,
reverse=False,
git=git):
"Invokes git diff on a filepath."
if commit:
ref, endref = commit+'^', commit
argv = []
if ref and endref:
argv.append('%s..%s' % (ref, endref))
elif ref:
for r in ref.strip().split():
argv.append(r)
elif branch:
argv.append(branch)
if filename:
argv.append('--')
if type(filename) is list:
argv.extend(filename)
else:
argv.append(filename)
start = False
del_tag = 'deleted file mode '
headers = []
deleted = cached and not os.path.exists(core.encode(filename))
diffoutput = git.diff(R=reverse, M=True, cached=cached,
*argv, **_common_diff_opts())
# Handle 'git init'
if diffoutput.startswith('fatal:'):
if with_diff_header:
return ('', '')
else:
return ''
if diffoutput.startswith('Submodule'):
if with_diff_header:
return ('', diffoutput)
else:
return diffoutput
output = StringIO()
diff = diffoutput.split('\n')
for line in map(core.decode, diff):
if not start and '@@' == line[:2] and '@@' in line[2:]:
start = True
if start or (deleted and del_tag in line):
output.write(core.encode(line) + '\n')
else:
if with_diff_header:
headers.append(core.encode(line))
elif not suppress_header:
output.write(core.encode(line) + '\n')
result = core.decode(output.getvalue())
output.close()
if with_diff_header:
return('\n'.join(headers), result)
else:
return result
def format_patchsets(to_export, revs, output='patches'):
"""
Group contiguous revision selection into patchsets
Exists to handle multi-selection.
Multiple disparate ranges in the revision selection
are grouped into continuous lists.
"""
outlines = []
cur_rev = to_export[0]
cur_master_idx = revs.index(cur_rev)
patches_to_export = [[cur_rev]]
patchset_idx = 0
# Group the patches into continuous sets
for idx, rev in enumerate(to_export[1:]):
# Limit the search to the current neighborhood for efficiency
master_idx = revs[cur_master_idx:].index(rev)
master_idx += cur_master_idx
if master_idx == cur_master_idx + 1:
patches_to_export[ patchset_idx ].append(rev)
cur_master_idx += 1
continue
else:
patches_to_export.append([ rev ])
cur_master_idx = master_idx
patchset_idx += 1
# Export each patchsets
status = 0
for patchset in patches_to_export:
newstatus, out = export_patchset(patchset[0],
patchset[-1],
output='patches',
n=len(patchset) > 1,
thread=True,
patch_with_stat=True)
outlines.append(out)
if status == 0:
status += newstatus
return (status, '\n'.join(outlines))
def export_patchset(start, end, output='patches', **kwargs):
"""Export patches from start^ to end."""
return git.format_patch('-o', output, start + '^..' + end,
with_stderr=True,
with_status=True,
**kwargs)
def unstage_paths(args, head='HEAD'):
status, output = git.reset(head, '--', with_stderr=True, with_status=True,
*set(args))
if status != 128:
return (status, output)
# handle git init: we have to use 'git rm --cached'
# detect this condition by checking if the file is still staged
status, output = git.update_index('--',
force_remove=True,
with_status=True,
with_stderr=True,
*set(args))
return (status, output)
def worktree_state(head='HEAD', staged_only=False):
"""Return a tuple of files in various states of being
Can be staged, unstaged, untracked, unmerged, or changed
upstream.
"""
state = worktree_state_dict(head=head, staged_only=staged_only)
return(state.get('staged', []),
state.get('modified', []),
state.get('unmerged', []),
state.get('untracked', []),
state.get('upstream_changed', []))
def worktree_state_dict(head='HEAD',
staged_only=False,
update_index=False,
git=git):
"""Return a dict of files in various states of being
:rtype: dict, keys are staged, unstaged, untracked, unmerged,
changed_upstream, and submodule.
"""
if update_index:
git.update_index(refresh=True)
if staged_only:
return _branch_status(head)
staged, unmerged, submodules = diff_index(head)
modified, more_submods = diff_worktree()
submodules = submodules.union(more_submods)
untracked = untracked_files()
# Look for upstream modified files if this is a tracking branch
upstream_changed = diff_upstream(head)
# Keep stuff sorted
staged.sort()
modified.sort()
unmerged.sort()
untracked.sort()
upstream_changed.sort()
return {'staged': staged,
'modified': modified,
'unmerged': unmerged,
'untracked': untracked,
'upstream_changed': upstream_changed,
'submodules': submodules}
def diff_index(head):
submodules = set()
staged = []
unmerged = []
output = git.diff_index(head, z=True, cached=True, with_stderr=True)
if output.startswith('fatal:'):
# handle git init
return all_files(), unmerged, submodules
while output:
rest, output = output.split('\0', 1)
name, output = output.split('\0', 1)
status = rest[-1]
name = core.decode(name)
if '160000' in rest[1:14]:
submodules.add(name)
elif status in 'DAM':
staged.append(name)
elif status == 'U':
unmerged.append(name)
return staged, unmerged, submodules
def diff_worktree():
modified = []
submodules = set()
output = git.diff_files(z=True, with_stderr=True)
if output.startswith('fatal:'):
# handle git init
ls_files = git.ls_files(modified=True, z=True)[:-1].split('\0')
modified = [core.decode(f) for f in ls_files if f]
return modified, submodules
while output:
rest, output = output.split('\0', 1)
name, output = output.split('\0', 1)
status = rest[-1]
name = core.decode(name)
if '160000' in rest[1:14]:
submodules.add(name)
elif status in 'DAM':
modified.append(name)
return modified, submodules
def diff_upstream(head):
tracked = tracked_branch()
if not tracked:
return []
diff_expr = merge_base_to(head, tracked)
output = git.diff(diff_expr, name_only=True, z=True,
**_common_diff_opts())
if output.startswith('fatal:'):
return []
return [core.decode(n) for n in output.split('\0') if n]
def partial_worktree_state_dict(files, head='HEAD'):
states = []
for path in files:
output = git.status(path, porcelain=True)
if output == "" or output[1] == " ":
states.append(('unmodified', path))
status = None
else:
status = output[1]
if status == "M" or status == "D" or status == "R":
states.append(('modified', path))
elif status == "?":
states.append(('untracked', path))
return states
def _branch_status(branch, git=git):
"""
Returns a tuple of staged, unstaged, untracked, and unmerged files
This shows only the changes that were introduced in branch
"""
status, output = git.diff(name_only=True,
M=True, z=True,
with_status=True,
*branch.strip().split(),
**_common_diff_opts())
if status != 0:
return {}
staged = map(core.decode, [n for n in output.split('\0') if n])
return {'staged': staged,
'upstream_changed': staged}
def merge_base_to(head, ref):
"""Given `ref`, return $(git merge-base ref HEAD)..ref."""
base = git.merge_base(head, ref)
return '%s..%s' % (base, ref)
def merge_base_parent(branch):
tracked = tracked_branch(branch=branch)
if tracked:
return '%s..%s' % (tracked, branch)
return 'master..%s' % branch
def eval_path(path):
"""handles quoted paths."""
if path.startswith('"') and path.endswith('"'):
return core.decode(eval(path))
else:
return core.decode(path)
def renamed_files(start, end, git=git):
difflines = git.diff('%s..%s' % (start, end), M=True,
**_common_diff_opts()).splitlines()
return [eval_path(r[12:].rstrip())
for r in difflines if r.startswith('rename from ')]
def changed_files(start, end, git=git):
zfiles_str = git.diff('%s..%s' % (start, end),
name_only=True, z=True,
**_common_diff_opts()).strip('\0')
return [core.decode(enc) for enc in zfiles_str.split('\0') if enc]
def parse_ls_tree(rev):
"""Return a list of(mode, type, sha1, path) tuples."""
lines = git.ls_tree(rev, r=True).splitlines()
output = []
regex = re.compile('^(\d+)\W(\w+)\W(\w+)[ \t]+(.*)$')
for line in lines:
match = regex.match(line)
if match:
mode = match.group(1)
objtype = match.group(2)
sha1 = match.group(3)
filename = match.group(4)
output.append((mode, objtype, sha1, filename,) )
return output
# A regex for matching the output of git(log|rev-list) --pretty=oneline
REV_LIST_REGEX = re.compile('^([0-9a-f]{40}) (.*)$')
def parse_rev_list(raw_revs):
"""Parse `git log --pretty=online` output into (SHA-1, summary) pairs."""
revs = []
for line in map(core.decode, raw_revs.splitlines()):
match = REV_LIST_REGEX.match(line)
if match:
rev_id = match.group(1)
summary = match.group(2)
revs.append((rev_id, summary,))
return revs
def log_helper(all=False, extra_args=None):
"""Return parallel arrays containing the SHA-1s and summaries."""
revs = []
summaries = []
args = []
if extra_args:
args = extra_args
output = git.log(pretty='oneline', no_color=True, all=all, *args)
for line in map(core.decode, output.splitlines()):
match = REV_LIST_REGEX.match(line)
if match:
revs.append(match.group(1))
summaries.append(match.group(2))
return (revs, summaries)
def rev_list_range(start, end):
"""Return a (SHA-1, summary) pairs between start and end."""
revrange = '%s..%s' % (start, end)
raw_revs = git.rev_list(revrange, pretty='oneline')
return parse_rev_list(raw_revs)
def merge_message_path():
"""Return the path to .git/MERGE_MSG or .git/SQUASH_MSG."""
for basename in ('MERGE_MSG', 'SQUASH_MSG'):
path = git.git_path(basename)
if os.path.exists(path):
return path
return None
def abort_merge():
"""Abort a merge by reading the tree at HEAD."""
# Reset the worktree
git.read_tree('HEAD', reset=True, u=True, v=True)
# remove MERGE_HEAD
merge_head = git.git_path('MERGE_HEAD')
if os.path.exists(merge_head):
os.unlink(merge_head)
# remove MERGE_MESSAGE, etc.
merge_msg_path = merge_message_path()
while merge_msg_path:
os.unlink(merge_msg_path)
merge_msg_path = merge_message_path()
def merge_message(revision):
"""Return a merge message for FETCH_HEAD."""
fetch_head = git.git_path('FETCH_HEAD')
if os.path.exists(fetch_head):
return git.fmt_merge_msg('--file', fetch_head)
return "Merge branch '%s'" % revision
-83
View File
@@ -1,83 +0,0 @@
import os
from cola import git
class GitCola(git.Git):
"""
Subclass git.Git to provide custom behaviors.
GitPython throws exceptions by default.
We suppress exceptions in favor of return values.
"""
def __init__(self):
git.Git.__init__(self)
self.load_worktree(os.getcwd())
def load_worktree(self, path):
self._git_dir = path
self._worktree = None
self.worktree()
def worktree(self):
if self._worktree:
return self._worktree
self.git_dir()
if self._git_dir:
curdir = self._git_dir
else:
curdir = os.getcwd()
if self._is_git_dir(os.path.join(curdir, '.git')):
return curdir
# Handle bare repositories
if (len(os.path.basename(curdir)) > 4
and curdir.endswith('.git')):
return curdir
if 'GIT_WORK_TREE' in os.environ:
self._worktree = os.getenv('GIT_WORK_TREE')
if not self._worktree or not os.path.isdir(self._worktree):
if self._git_dir:
gitparent = os.path.join(os.path.abspath(self._git_dir), '..')
self._worktree = os.path.abspath(gitparent)
self.set_cwd(self._worktree)
return self._worktree
def is_valid(self):
return self._git_dir and self._is_git_dir(self._git_dir)
def git_dir(self):
if self.is_valid():
return self._git_dir
if 'GIT_DIR' in os.environ:
self._git_dir = os.getenv('GIT_DIR')
if self._git_dir:
curpath = os.path.abspath(self._git_dir)
else:
curpath = os.path.abspath(os.getcwd())
# Search for a .git directory
while curpath:
if self._is_git_dir(curpath):
self._git_dir = curpath
break
gitpath = os.path.join(curpath, '.git')
if self._is_git_dir(gitpath):
self._git_dir = gitpath
break
curpath, dummy = os.path.split(curpath)
if not dummy:
break
return self._git_dir
def _is_git_dir(self, d):
""" This is taken from the git setup.c:is_git_directory
function."""
if (os.path.isdir(d)
and os.path.isdir(os.path.join(d, 'objects'))
and os.path.isdir(os.path.join(d, 'refs'))):
headref = os.path.join(d, 'HEAD')
return (os.path.isfile(headref)
or (os.path.islink(headref)
and os.readlink(headref).startswith('refs')))
return False
+312
View File
@@ -0,0 +1,312 @@
import os
from PyQt4 import QtGui
import cola
import cola.app
from cola import core
from cola import git
from cola import gitcmds
from cola import qtutils
from cola import signals
from cola.widgets.browse import BrowseDialog
from cola.widgets.combodlg import ComboDialog
from cola.widgets.listview import ListView
def install_command_wrapper(parent):
wrapper = CommandWrapper(parent)
cola.factory().add_command_wrapper(wrapper)
class CommandWrapper(object):
def __init__(self, parent):
self.parent = parent
self.callbacks = {
signals.question: self._question,
signals.information: self._information,
}
def _question(self, title, msg):
return qtutils.question(self.parent, title, msg)
def _information(self, title, msg):
if msg is None:
msg = title
title = qtutils.tr(title)
msg = qtutils.tr(msg)
QtGui.QMessageBox.information(self.parent, title, msg)
def choose_from_combo(title, items):
"""Quickly choose an item from a list using a combo box"""
parent = QtGui.QApplication.instance().activeWindow()
return ComboDialog(parent, title=title, items=items).selected()
def choose_from_list(title, items=None, dblclick=None):
"""Quickly choose an item from a list using a list widget"""
parent = QtGui.QApplication.instance().activeWindow()
return ListView(parent,
title=title, items=items, dblclick=dblclick).selected()
def slot_with_parent(fn, parent):
"""Return an argument-less method for calling fn(parent=parent)
:param fn: - Function reference, must accept 'parent' as a keyword
:param parent: - Qt parent widget
"""
def slot():
fn(parent=parent)
return slot
def branch_delete():
"""Launch the 'Delete Branch' dialog."""
branch = choose_from_combo('Delete Branch',
cola.model().local_branches)
if not branch:
return
cola.notifier().broadcast(signals.delete_branch, branch)
def branch_diff():
"""Diff against an arbitrary revision, branch, tag, etc."""
branch = choose_from_combo('Select Branch, Tag, or Commit-ish',
['HEAD^'] +
cola.model().all_branches() +
cola.model().tags)
if not branch:
return
cola.notifier().broadcast(signals.diff_mode, branch)
def browse_current():
"""Launch the 'Browse Current Branch' dialog."""
branch = gitcmds.current_branch()
BrowseDialog.browse(branch)
def browse_other():
"""Prompt for a branch and inspect content at that point in time."""
# Prompt for a branch to browse
branch = choose_from_combo('Browse Revision...', gitcmds.all_refs())
if not branch:
return
BrowseDialog.browse(branch)
def checkout_branch():
"""Launch the 'Checkout Branch' dialog."""
branch = choose_from_combo('Checkout Branch',
cola.model().local_branches)
if not branch:
return
cola.notifier().broadcast(signals.checkout_branch, branch)
def cherry_pick():
"""Launch the 'Cherry-Pick' dialog."""
from cola.controllers.selectcommits import select_commits
revs, summaries = gitcmds.log_helper(all=True)
commits = select_commits('Cherry-Pick Commit',
revs, summaries, multiselect=False)
if not commits:
return
cola.notifier().broadcast(signals.cherry_pick, commits)
def clone_repo(spawn=True):
"""
Present GUI controls for cloning a repository
A new cola session is invoked when 'spawn' is True.
"""
url, ok = qtutils.prompt('Path or URL to clone (Env. $VARS okay)')
url = os.path.expandvars(core.encode(url))
if not ok or not url:
return None
try:
# Pick a suitable basename by parsing the URL
newurl = url.replace('\\', '/')
default = newurl.rsplit('/', 1)[-1]
if default == '.git':
# The end of the URL is /.git, so assume it's a file path
default = os.path.basename(os.path.dirname(newurl))
if default.endswith('.git'):
# The URL points to a bare repo
default = default[:-4]
if url == '.':
# The URL is the current repo
default = os.path.basename(os.getcwd())
if not default:
raise
except:
cola.notifier().broadcast(signals.information,
'Error Cloning',
'Could not parse: "%s"' % url)
qtutils.log(1, 'Oops, could not parse git url: "%s"' % url)
return None
# Prompt the user for a directory to use as the parent directory
parent = QtGui.QApplication.instance().activeWindow()
msg = 'Select a parent directory for the new clone'
dirname = qtutils.opendir_dialog(parent, msg, cola.model().getcwd())
if not dirname:
return None
count = 1
dirname = core.decode(dirname)
destdir = os.path.join(dirname, core.decode(default))
olddestdir = destdir
if os.path.exists(destdir):
# An existing path can be specified
msg = ('"%s" already exists, cola will create a new directory' %
destdir)
cola.notifier().broadcast(signals.information,
'Directory Exists', msg)
# Make sure the new destdir doesn't exist
while os.path.exists(destdir):
destdir = olddestdir + str(count)
count += 1
cola.notifier().broadcast(signals.clone, core.decode(url), destdir,
spawn=spawn)
return destdir
def export_patches():
"""Run 'git format-patch' on a list of commits."""
from cola.controllers.selectcommits import select_commits
revs, summaries = gitcmds.log_helper()
to_export = select_commits('Export Patches', revs, summaries)
if not to_export:
return
to_export.reverse()
revs.reverse()
cola.notifier().broadcast(signals.format_patch, to_export, revs)
def diff_branch():
"""Launches a diff against a branch."""
branch = choose_from_combo('Select Branch, Tag, or Commit-ish',
['HEAD^'] +
cola.model().all_branches() +
cola.model().tags)
if not branch:
return
zfiles_str = git.instance().diff(branch, name_only=True,
no_color=True,
z=True).rstrip('\0')
files = zfiles_str.split('\0')
filename = choose_from_list('Select File', files)
if not filename:
return
cola.notifier().broadcast(signals.branch_mode, branch, filename)
def diff_expression():
"""Diff using an arbitrary expression."""
expr = choose_from_combo('Enter Diff Expression',
cola.model().all_branches() +
cola.model().tags)
if not expr:
return
cola.notifier().broadcast(signals.diff_expr_mode, expr)
def goto_grep(line):
"""Called when Search -> Grep's right-click 'goto' action."""
filename, line_number, contents = line.split(':', 2)
filename = core.encode(filename)
cola.notifier().broadcast(signals.edit, [filename], line_number=line_number)
def grep():
"""Prompt and use 'git grep' to find the content."""
# This should be a command in cola.cmds.
txt, ok = qtutils.prompt('grep')
if not ok:
return
cola.notifier().broadcast(signals.grep, txt)
def open_repo(parent):
"""Spawn a new cola session."""
dirname = qtutils.opendir_dialog(parent,
'Open Git Repository...',
cola.model().getcwd())
if not dirname:
return
cola.notifier().broadcast(signals.open_repo, dirname)
def open_repo_slot(parent):
return slot_with_parent(open_repo, parent)
def load_commitmsg(parent):
"""Load a commit message from a file."""
filename = qtutils.open_dialog(parent,
'Load Commit Message...',
cola.model().getcwd())
if filename:
cola.notifier().broadcast(signals.load_commit_message, filename)
def load_commitmsg_slot(parent):
return slot_with_parent(load_commitmsg, parent)
def rebase():
"""Rebase onto a branch."""
branch = choose_from_combo('Rebase Branch',
cola.model().all_branches())
if not branch:
return
#TODO cmd
status, output = git.instance().rebase(branch,
with_stderr=True,
with_status=True)
qtutils.log(status, output)
def review_branch():
"""Diff against an arbitrary revision, branch, tag, etc."""
branch = choose_from_combo('Select Branch, Tag, or Commit-ish',
cola.model().all_branches() +
cola.model().tags)
if not branch:
return
cola.notifier().broadcast(signals.review_branch_mode, branch)
def fetch(parent):
"""Launch the 'fetch' remote dialog."""
from cola.controllers.remote import remote_action
remote_action(parent, 'fetch')
def fetch_slot(parent):
return slot_with_parent(fetch, parent)
def push(parent):
"""Launch the 'push' remote dialog."""
from cola.controllers.remote import remote_action
remote_action(parent, 'push')
def push_slot(parent):
return slot_with_parent(push, parent)
def pull(parent):
"""Launch the 'pull' remote dialog."""
from cola.controllers.remote import remote_action
remote_action(parent, 'pull')
def pull_slot(parent):
return slot_with_parent(pull, parent)
+89
View File
@@ -0,0 +1,89 @@
"""i18n and l10n support for git-cola"""
import gettext as _gettext
import os
import sys
from cola import resources
_null_translation = _gettext.NullTranslations()
_translation = _null_translation
def gettext(s):
return _translation.ugettext(s)
def ngettext(s, p, n):
return _translation.ungettext(s, p, n)
def N_(s):
return s
def install(locale):
global _translation
if sys.platform == 'win32':
_check_win32_locale()
if locale:
os.environ['LANG'] = locale
os.environ['LC_MESSAGES'] = locale
_gettext.textdomain('messages')
_translation = _gettext.translation('git-cola',
localedir=_get_locale_dir(),
fallback=True)
def uninstall():
global _translation
_translation = _null_translation
def _get_locale_dir():
return resources.prefix('share', 'locale')
def _check_win32_locale():
for i in ('LANGUAGE','LC_ALL','LC_MESSAGES','LANG'):
if os.environ.get(i):
break
else:
lang = None
import locale
try:
import ctypes
except ImportError:
# use only user's default locale
lang = locale.getdefaultlocale()[0]
else:
# using ctypes to determine all locales
lcid_user = ctypes.windll.kernel32.GetUserDefaultLCID()
lcid_system = ctypes.windll.kernel32.GetSystemDefaultLCID()
if lcid_user != lcid_system:
lcid = [lcid_user, lcid_system]
else:
lcid = [lcid_user]
lang = [locale.windows_locale.get(i) for i in lcid]
lang = ':'.join([i for i in lang if i])
# set lang code for gettext
if lang:
os.environ['LANGUAGE'] = lang
# additional strings for translation
if 0:
# file kinds
N_('file')
N_('directory')
N_('symlink')
# bugs status
N_('fixed')
# qcat titles for various file types
N_('View text file')
N_('View image file')
N_('View binary file')
N_('View symlink')
N_('View directory')
#
N_("No changes selected to commit")
N_("No changes selected to revert")
+144 -70
View File
@@ -2,7 +2,9 @@
"""Provides an inotify plugin for Linux and other systems with pyinotify"""
import os
import time
from threading import Timer
from threading import Lock
from cola import utils
try:
import pyinotify
@@ -14,25 +16,37 @@ try:
except ImportError:
ProcessEvent = object
AVAILABLE = False
pass
if utils.is_win32():
try:
import win32file
import win32con
import pywintypes
import win32event
AVAILABLE = True
except ImportError:
ProcessEvent = object
AVAILABLE = False
from PyQt4 import QtCore
import cola
from cola import core
from cola import signals
from cola import utils
INOTIFY_EVENT = QtCore.QEvent.User + 0
from cola.compat import set
_thread = None
def start():
global _thread
if not AVAILABLE:
if not utils.is_linux():
if utils.is_win32():
msg = ('file notification: disabled\n'
'Note: install pywin32 to enable.\n')
elif utils.is_linux():
msg = ('inotify: disabled\n'
'Note: install python-pyinotify to enable inotify.\n')
else:
return
msg = ('inotify: disabled\n'
'Note: install python-pyinotify to enable inotify.\n')
if utils.is_debian():
msg += ('On Debian systems '
@@ -43,65 +57,75 @@ def start():
# Start the notification thread
_thread = GitNotifier()
_thread.start()
msg = 'inotify support: enabled'
if utils.is_win32():
msg = 'file notification: enabled'
else:
msg = 'inotify support: enabled'
cola.notifier().broadcast(signals.log_cmd, 0, msg)
cola.notifier().broadcast(signals.inotify, True)
def stop():
if not has_inotify():
return
_thread.set_abort(True)
_thread.quit()
_thread.stop(True)
_thread.wait()
def has_inotify():
"""Return True if pyinotify is available."""
return AVAILABLE and _thread and _thread.isRunning()
class EventReceiver(QtCore.QObject):
def event(self, msg):
"""Overrides event() to handle custom inotify events."""
if not AVAILABLE:
return
if msg.type() == INOTIFY_EVENT:
cola.notifier().broadcast(signals.rescan)
return True
else:
return False
class Handler():
"""Queues filesystem events for broadcast"""
def __init__(self):
"""Create an event handler"""
## Timer used to prevent notification floods
self._timer = None
## The files that have changed since the last broadcast
self._files = set()
## Lock to protect files and timer from threading issues
self._lock = Lock()
def broadcast(self):
"""Broadcasts a list of all files touched since last broadcast"""
with self._lock:
cola.notifier().broadcast(signals.update_status, self._files)
self._files = set()
self._timer = None
def handle(self, path):
"""Queues up filesystem events for broadcast"""
with self._lock:
self._files.add(path)
if self._timer is None:
self._timer = Timer(0.333, self.broadcast)
self._timer.start()
class FileSysEvent(ProcessEvent):
"""Generated by GitNotifier in response to inotify events"""
def __init__(self, parent):
"""Keep a reference to the QThread parent and maintain event state"""
def __init__(self):
"""Maintain event state"""
ProcessEvent.__init__(self)
## The Qt parent
self._parent = parent
## Timer used to prevents notification floods
self._last_event_time = time.time()
## Takes care of Queueing events for broadcast
self._handler = Handler()
def process_default(self, event):
"""Notifies the Qt parent when actions occur"""
## Limit the notification frequency to 1 per second
if time.time() - self._last_event_time > 1.0:
self._parent.notify()
self._last_event_time = time.time()
"""Queues up inotify events for broadcast"""
if event.name is not None:
path = os.path.join(event.path, event.name)
path = os.path.relpath(path)
self._handler.handle(path)
class GitNotifier(QtCore.QThread):
"""Polls inotify for changes and generates FileSysEvents"""
def __init__(self, timeout=250):
def __init__(self, timeout=333):
"""Set up the pyinotify thread"""
QtCore.QThread.__init__(self)
self.setTerminationEnabled(True)
## QApplication receiver of Qt events
self._receiver = EventReceiver()
## Git command object
self._git = cola.model().git
## pyinotify timeout
@@ -109,33 +133,31 @@ class GitNotifier(QtCore.QThread):
## Path to monitor
self._path = self._git.worktree()
## Signals thread termination
self._abort = False
self._running = True
## Directories to watching
self._dirs_seen = set()
## The inotify watch manager instantiated in run()
self._wmgr = None
## Events to capture
self._mask = (EventsCodes.ALL_FLAGS['IN_CREATE'] |
EventsCodes.ALL_FLAGS['IN_DELETE'] |
EventsCodes.ALL_FLAGS['IN_MODIFY'] |
EventsCodes.ALL_FLAGS['IN_MOVED_TO'])
if utils.is_linux():
self._mask = (EventsCodes.ALL_FLAGS['IN_ATTRIB'] |
EventsCodes.ALL_FLAGS['IN_CLOSE_WRITE'] |
EventsCodes.ALL_FLAGS['IN_DELETE'] |
EventsCodes.ALL_FLAGS['IN_MODIFY'] |
EventsCodes.ALL_FLAGS['IN_MOVED_TO'])
def set_abort(self, abort):
"""Tells the GitNotifier to abort"""
self._abort = abort
def notify(self):
"""Post a Qt event in response to inotify updates"""
if not self._abort:
event_type = QtCore.QEvent.Type(INOTIFY_EVENT)
event = QtCore.QEvent(event_type)
QtCore.QCoreApplication.postEvent(self._receiver, event)
def stop(self, stopped):
"""Tells the GitNotifier to stop"""
self._timeout = 0
self._running = not stopped
def _watch_directory(self, directory):
"""Set up a directory for monitoring by inotify"""
if not self._wmgr:
return
directory = os.path.realpath(directory)
if not os.path.exists(directory):
return
if directory not in self._dirs_seen:
self._wmgr.add_watch(directory, self._mask)
self._dirs_seen.add(directory)
@@ -153,30 +175,82 @@ class GitNotifier(QtCore.QThread):
def run(self):
"""Create the inotify WatchManager and generate FileSysEvents"""
if utils.is_win32():
self.run_win32()
return
# Only capture events that git cares about
self._wmgr = WatchManager()
if self._is_pyinotify_08x():
notifier = Notifier(self._wmgr, FileSysEvent(self),
notifier = Notifier(self._wmgr, FileSysEvent(),
timeout=self._timeout)
else:
notifier = Notifier(self._wmgr, FileSysEvent(self))
dirs_seen = set()
added_flag = False
# self._abort signals app termination. The timeout is a tradeoff
notifier = Notifier(self._wmgr, FileSysEvent())
self._watch_directory(self._path)
# Register files/directories known to git
for filename in core.decode(self._git.ls_files()).splitlines():
filename = os.path.realpath(filename)
directory = os.path.dirname(filename)
self._watch_directory(directory)
# self._running signals app termination. The timeout is a tradeoff
# between fast notification response and waiting too long to exit.
while not self._abort:
if not added_flag:
self._watch_directory(self._path)
# Register files/directories known to git
for filename in self._git.ls_files().splitlines():
directory = os.path.dirname(filename)
self._watch_directory(directory)
added_flag = True
notifier.process_events()
while self._running:
if self._is_pyinotify_08x():
check = notifier.check_events()
else:
check = notifier.check_events(timeout=self._timeout)
if not self._running:
break
if check:
notifier.read_events()
notifier.process_events()
notifier.stop()
def run_win32(self):
"""Generate notifications using pywin32"""
hDir = win32file.CreateFile(
self._path,
0x0001,
win32con.FILE_SHARE_READ | win32con.FILE_SHARE_WRITE,
None,
win32con.OPEN_EXISTING,
win32con.FILE_FLAG_BACKUP_SEMANTICS |
win32con.FILE_FLAG_OVERLAPPED,
None)
flags = (win32con.FILE_NOTIFY_CHANGE_FILE_NAME |
win32con.FILE_NOTIFY_CHANGE_DIR_NAME |
win32con.FILE_NOTIFY_CHANGE_ATTRIBUTES |
win32con.FILE_NOTIFY_CHANGE_SIZE |
win32con.FILE_NOTIFY_CHANGE_LAST_WRITE |
win32con.FILE_NOTIFY_CHANGE_SECURITY)
buf = win32file.AllocateReadBuffer(8192)
overlapped = pywintypes.OVERLAPPED()
overlapped.hEvent = win32event.CreateEvent(None, 0, 0, None)
handler = Handler()
while self._running:
win32file.ReadDirectoryChangesW(hDir,
buf,
True,
flags,
overlapped)
rc = win32event.WaitForSingleObject(overlapped.hEvent, self._timeout)
if rc == win32event.WAIT_OBJECT_0:
nbytes = win32file.GetOverlappedResult(hDir, overlapped, True)
if nbytes:
results = win32file.FILE_NOTIFY_INFORMATION(buf, nbytes)
for action, path in results:
if not self._running:
break
path = path.replace('\\', '/')
if not path.startswith('.git/') and os.path.isfile(path):
handler.handle(path)
-202
View File
@@ -1,202 +0,0 @@
# Copyright (C) 2009, David Aguilar <davvid@gmail.com>
"""Provides the main() routine used by the git-cola script"""
import optparse
import signal
import glob
import sys
import os
from cola import resources
from cola import utils
from cola import core
from cola import git
from cola import inotify
from cola import version
# Spoof an X11 display for SSH
os.environ.setdefault('DISPLAY', ':0')
# Provide an SSH_ASKPASS fallback
if sys.platform == 'darwin':
os.environ.setdefault('SSH_ASKPASS',
resources.share('bin', 'ssh-askpass-darwin'))
else:
os.environ.setdefault('SSH_ASKPASS',
resources.share('bin', 'ssh-askpass'))
def main():
"""Parses the command-line arguments and starts git-cola
"""
parser = optparse.OptionParser(usage='%prog [options]')
# We also accept 'git cola version'
parser.add_option('-v', '--version',
help='Show cola version',
dest='version',
default=False,
action='store_true')
# Accept git cola --classic
parser.add_option('--classic',
help='Launch cola classic',
dest='classic',
default=False,
action='store_true')
# Accept --style=/path/to/style.qss or --style=dark for built-in styles
parser.add_option('-s', '--style',
help='Applies an alternate stylesheet. '
'The allowed values are: "dark" or a file path.',
dest='style',
metavar='PATH or STYLE',
default='')
# Specifies a git repository to open
parser.add_option('-r', '--repo',
help='Specifies the path to a git repository.',
dest='repo',
metavar='PATH',
default=os.getcwd())
# Used on Windows for adding 'git' to the path
parser.add_option('-g', '--git-path',
help='Specifies the path to the git binary',
dest='git',
metavar='PATH',
default='')
opts, args = parser.parse_args()
if opts.version or 'version' in args:
# Accept 'git cola --version' or 'git cola version'
git.Git.execute(['git', 'update-index', '--refresh'])
print 'cola version', version.version()
sys.exit(0)
if opts.git:
# Adds git to the PATH. This is needed on Windows.
path_entries = os.environ.get('PATH', '').split(os.pathsep)
path_entries.insert(0, os.path.dirname(opts.git))
os.environ['PATH'] = os.pathsep.join(path_entries)
# Bail out if --repo is not a directory
repo = os.path.realpath(opts.repo)
if not os.path.isdir(repo):
print >> sys.stderr, "fatal: '%s' is not a directory. Consider supplying -r <path>.\n" % repo
sys.exit(-1)
# We do everything relative to the repo root
os.chdir(opts.repo)
try:
# Defer these imports to allow git cola --version without pyqt installed
from PyQt4 import QtCore
from PyQt4 import QtGui
except ImportError:
print >> sys.stderr, 'Sorry, you do not seem to have PyQt4 installed.'
print >> sys.stderr, 'Please install it before using cola.'
print >> sys.stderr, 'e.g.: sudo apt-get install python-qt4'
sys.exit(-1)
# Import cola modules
import cola
from cola.models.gitrepo import GitRepoModel
from cola.views.main import MainView
from cola.views.repo import RepoTreeView
from cola.controllers.main import MainController
from cola.controllers.classic import ClassicController
from cola.app import ColaApplication
from cola import qtutils
from cola import commands
# Allow Ctrl-C to exit
signal.signal(signal.SIGINT, signal.SIG_DFL)
# Initialize the app
app = ColaApplication(sys.argv)
style = None
if opts.style:
# This loads the built-in and user-specified stylesheets.
# We allows absolute and relative paths to a stylesheet
# by assuming that non-file arguments refer to a built-in style.
if os.path.isabs(opts.style) or os.path.isfile(opts.style):
filename = opts.style
else:
filename = resources.stylesheet(opts.style)
if filename and os.path.exists(filename):
# Automatically register each subdirectory in the style dir
# as a Qt resource directory.
_setup_resource_dir(os.path.dirname(filename))
stylesheet = open(filename, 'r')
style = core.read_nointr(stylesheet)
stylesheet.close()
app.setStyleSheet(style)
else:
_setup_resource_dir(resources.style_dir())
print >> sys.stderr, ("warn: '%s' is not a valid style."
% opts.style)
else:
# Add the default style dir so that we find our icons
_setup_resource_dir(resources.style_dir())
# Ensure that we're working in a valid git repository.
# If not, try to find one. When found, chdir there.
model = cola.model()
valid = model.use_worktree(repo)
while not valid:
gitdir = qtutils.opendir_dialog(app.activeWindow(),
'Open Git Repository...',
os.getcwd())
if not gitdir:
sys.exit(-1)
valid = model.use_worktree(gitdir)
# Finally, go to the root of the git repo
os.chdir(model.git.worktree())
# Register model commands
commands.register()
# Show the GUI and start the event loop
if opts.classic:
view = RepoTreeView()
view.setModel(GitRepoModel(view))
controller = ClassicController(view)
else:
view = MainView()
controller = MainController(model, view)
# Scan for the first time
model.update_status()
# Start the inotify thread
inotify.start()
# Make sure that we start out on top
view.raise_()
# Show the view and start the main event loop
view.show()
result = app.exec_()
# All done, cleanup
inotify.stop()
QtCore.QThreadPool.globalInstance().waitForDone()
pattern = cola.model().tmp_file_pattern()
for filename in glob.glob(pattern):
os.unlink(filename)
sys.exit(result)
def _setup_resource_dir(dirname):
"""Adds resource directories to Qt's search path"""
from PyQt4 import QtCore
resource_paths = resources.resource_dirs(dirname)
for r in resource_paths:
basename = os.path.basename(r)
QtCore.QDir.setSearchPaths(basename, [r])
View File
+26
View File
@@ -0,0 +1,26 @@
"""Provides the main application controller."""
from cola.ctrl import Controller
from cola import signals
class MainController(Controller):
def __init__(self, model, view):
Controller.__init__(self, model, view)
self.add_global_command(signals.amend_mode)
self.add_global_command(signals.diffstat)
self.add_global_command(signals.load_commit_message)
self.add_global_command(signals.load_commit_template)
self.add_global_command(signals.load_previous_message)
self.add_global_command(signals.rescan)
self.add_global_command(signals.reset_mode)
self.add_global_command(signals.run_config_action)
self.add_global_command(signals.signoff)
self.add_global_command(signals.stage_untracked)
self.add_global_command(signals.stage_modified)
self.add_global_command(signals.stage_untracked)
self.add_global_command(signals.unstage_all)
self.add_global_command(signals.unstage_selected)
self.add_global_command(signals.visualize_all)
self.add_global_command(signals.visualize_current)
+558
View File
@@ -0,0 +1,558 @@
# Copyright (c) 2008 David Aguilar
"""This module provides the central cola model.
"""
import os
import time
import copy
from cola import core
from cola import utils
from cola import git
from cola import gitcfg
from cola import gitcmds
from cola import serializer
from cola.compat import set
from cola.obsmodel import ObservableModel, OMSerializer
from cola.decorators import memoize
# Static GitConfig instance
_config = gitcfg.instance()
# Provides access to a global MainModel instance
@memoize
def model():
"""Returns the main model singleton"""
return MainModel()
class MainSerializer(OMSerializer):
def post_decode_hook(self):
OMSerializer.post_decode_hook(self)
self.obj.generate_remote_helpers()
class MainModel(ObservableModel):
"""Provides a friendly wrapper for doing common git operations."""
# Observable messages
message_about_to_update = 'about_to_update'
message_commit_message_changed = 'commit_message_changed'
message_diff_text_changed = 'diff_text_changed'
message_filename_changed = 'filename_changed'
message_head_changed = 'head_changed'
message_mode_changed = 'mode_changed'
message_updated = 'updated'
# States
mode_none = 'none' # Default: nothing's happened, do nothing
mode_worktree = 'worktree' # Comparing index to worktree
mode_index = 'index' # Comparing index to last commit
mode_amend = 'amend' # Amending a commit
mode_grep = 'grep' # We ran Search -> Grep
mode_branch = 'branch' # Applying changes from a branch
mode_diff = 'diff' # Diffing against an arbitrary branch
mode_diff_expr = 'diff_expr' # Diffing using arbitrary expression
mode_review = 'review' # Reviewing a branch
# Modes where we don't do anything like staging, etc.
modes_read_only = (mode_branch, mode_grep,
mode_diff, mode_diff_expr, mode_review)
# Modes where we can checkout files from the $head
modes_undoable = (mode_none, mode_index, mode_worktree)
unstaged = property(lambda self: self.modified + self.unmerged + self.untracked)
"""An aggregate of the modified, unmerged, and untracked file lists."""
def __init__(self, cwd=None):
"""Reads git repository settings and sets several methods
so that they refer to the git module. This object
encapsulates cola's interaction with git."""
ObservableModel.__init__(self)
# Initialize the git command object
self.git = git.instance()
#####################################################
self.head = 'HEAD'
self.diff_text = ''
self.mode = self.mode_none
self.filename = None
self.currentbranch = ''
self.trackedbranch = ''
self.directory = ''
self.git_version = self.git.version()
self.remotes = []
self.remotename = ''
self.local_branch = ''
self.remote_branch = ''
#####################################################
# Status info
self.commitmsg = ''
self.modified = []
self.staged = []
self.untracked = []
self.unmerged = []
self.upstream_changed = []
self.submodules = set()
#####################################################
# Refs
self.revision = ''
self.local_branches = []
self.remote_branches = []
self.tags = []
self.revisions = []
self.summaries = []
self.fetch_helper = None
self.push_helper = None
self.pull_helper = None
self.generate_remote_helpers()
if cwd:
self.use_worktree(cwd)
#####################################################
# Dag
self._commits = []
def read_only(self):
return self.mode in self.modes_read_only
def undoable(self):
"""Whether we can checkout files from the $head."""
return self.mode in self.modes_undoable
def enable_staging(self):
"""Whether staging should be allowed."""
return self.mode == self.mode_worktree
def generate_remote_helpers(self):
"""Generates helper methods for fetch, push and pull"""
self.push_helper = self.gen_remote_helper(self.git.push, push=True)
self.fetch_helper = self.gen_remote_helper(self.git.fetch)
self.pull_helper = self.gen_remote_helper(self.git.pull)
def use_worktree(self, worktree):
self.git.load_worktree(worktree)
is_valid = self.git.is_valid()
if is_valid:
basename = os.path.basename(self.git.worktree())
self.set_project(core.decode(basename))
return is_valid
def editor(self):
app = _config.get('gui.editor', 'gvim')
return {'vim': 'gvim'}.get(app, app)
def history_browser(self):
return _config.get('gui.historybrowser', 'gitk')
def all_branches(self):
return (self.local_branches + self.remote_branches)
def set_commitmsg(self, msg):
self.commitmsg = msg
self.notify_message_observers(self.message_commit_message_changed, msg)
def set_diff_text(self, txt):
self.diff_text = txt
self.notify_message_observers(self.message_diff_text_changed, txt)
def set_filename(self, filename):
self.filename = filename
self.notify_message_observers(self.message_filename_changed, filename)
def set_head(self, head):
self.head = head
self.notify_message_observers(self.message_head_changed, head)
def set_mode(self, mode):
self.mode = mode
self.notify_message_observers(self.message_mode_changed, mode)
def set_remote(self, remote):
if not remote:
return
self.set_param('remote', remote)
branches = utils.grep('%s/\S+$' % remote,
gitcmds.branch_list(remote=True),
squash=False)
self.set_remote_branches(branches)
def apply_diff(self, filename):
return self.git.apply(filename, index=True, cached=True,
with_stderr=True, with_status=True)
def apply_diff_to_worktree(self, filename):
return self.git.apply(filename,
with_stderr=True, with_status=True)
def prev_commitmsg(self, *args):
"""Queries git for the latest commit message."""
log = self.git.log('-1', no_color=True, pretty='format:%s%n%n%b', *args)
return core.decode(log)
def update_file_status(self, update_index=False):
self.notify_message_observers(self.message_about_to_update)
self.notification_enabled = False
self._update_files(update_index=update_index)
self.notification_enabled = True
self.notify_observers('staged', 'unstaged')
self.broadcast_updated()
def update_status(self, update_index=False):
# Give observers a chance to respond
self.notify_message_observers(self.message_about_to_update)
# This allows us to defer notification until the
# we finish processing data
self.notification_enabled = False
self._update_files(update_index=update_index)
self._update_refs()
self._update_branches_and_tags()
self._update_branch_heads()
# Re-enable notifications and emit changes
self.notification_enabled = True
self.notify_observers('staged', 'unstaged')
self.broadcast_updated()
def update_status_of_files(self, files):
self.notify_message_observers(self.message_about_to_update)
self.notification_enabled = False
states = gitcmds.partial_worktree_state_dict(files, self.head)
for status, path in states:
if status == 'unmodified':
if path in self.modified:
self.modified.remove(path)
if path in self.untracked:
self.untracked.remove(path)
elif status == 'modified':
if path in self.untracked:
self.untracked.remove(path)
if path not in self.modified:
self.modified.append(path)
elif status == 'untracked':
if path in self.modified:
self.modified.remove(path)
if path not in self.untracked:
self.untracked.append(path)
self.modified.sort()
self.untracked.sort()
self.notification_enabled = True
self.notify_observers('staged', 'unstaged')
self.broadcast_updated()
def broadcast_updated(self):
self.notify_message_observers(self.message_updated)
def _update_files(self, worktree_only=False, update_index=False):
staged_only = self.read_only()
state = gitcmds.worktree_state_dict(head=self.head,
update_index=update_index,
staged_only=staged_only)
self.staged = state.get('staged', [])
self.modified = state.get('modified', [])
self.unmerged = state.get('unmerged', [])
self.untracked = state.get('untracked', [])
self.submodules = state.get('submodules', set())
self.upstream_changed = state.get('upstream_changed', [])
def _update_refs(self):
self.set_remotes(self.git.remote().splitlines())
self.set_revision('')
self.set_local_branch('')
self.set_remote_branch('')
def _update_branch_heads(self):
# Set these early since they are used to calculate 'upstream_changed'.
self.set_trackedbranch(gitcmds.tracked_branch())
self.set_currentbranch(gitcmds.current_branch())
def _update_branches_and_tags(self):
local_branches, remote_branches, tags = gitcmds.all_refs(split=True)
self.set_local_branches(local_branches)
self.set_remote_branches(remote_branches)
self.set_tags(tags)
def delete_branch(self, branch):
return self.git.branch(branch,
D=True,
with_stderr=True,
with_status=True)
def revision_sha1(self, idx):
return self.revisions[idx]
def _sliced_op(self, input_items, map_fn, size=42):
items = copy.copy(input_items)
full_status = 0
full_output = []
while len(items) > 0:
status, output = map_fn(items[:size])
full_status = full_status or status
full_output.append(output)
items = items[size:]
return (full_status, '\n'.join(full_output))
def _sliced_add(self, input_items, size=42):
lambda_fn = lambda x: self.git.add('--',
v=True,
with_stderr=True,
with_status=True,
*x)
return self._sliced_op(input_items, lambda_fn)
def stage_modified(self):
status, output = self._sliced_add(self.modified)
self.update_file_status()
return (status, output)
def stage_untracked(self):
status, output = self._sliced_add(self.untracked)
self.update_file_status()
return (status, output)
def reset(self, *items):
lambda_fn = lambda x: self.git.reset('--',
with_stderr=True,
with_status=True,
*x)
status, output = self._sliced_op(items, lambda_fn)
self.update_file_status()
return (status, output)
def unstage_all(self):
status, output = self.git.reset(self.head, '--', '.',
with_stderr=True,
with_status=True)
self.update_file_status()
return (status, output)
def stage_all(self):
status, output = self.git.add(v=True,
u=True,
with_stderr=True,
with_status=True)
self.update_file_status()
return (status, output)
def config_set(self, key, value, local=True):
# git config category.key value
strval = unicode(value)
if type(value) is bool:
# git uses "true" and "false"
strval = strval.lower()
if local:
argv = [key, strval]
else:
argv = ['--global', key, strval]
return self.git.config(*argv)
def config_dict(self, local=True):
"""parses the lines from git config --list into a dictionary"""
kwargs = {
'list': True,
'global': not local, # global is a python keyword
}
config_lines = self.git.config(**kwargs).splitlines()
newdict = {}
for line in config_lines:
try:
k, v = line.split('=', 1)
except:
# value-less entry in .gitconfig
continue
v = core.decode(v)
k = k.replace('.','_') # git -> model
if v == 'true' or v == 'false':
v = bool(eval(v.title()))
try:
v = int(eval(v))
except:
pass
newdict[k]=v
return newdict
def commit_with_msg(self, msg, amend=False):
"""Creates a git commit."""
if not msg.endswith('\n'):
msg += '\n'
# Sure, this is a potential "security risk," but if someone
# is trying to intercept/re-write commit messages on your system,
# then you probably have bigger problems to worry about.
tmpfile = self.tmp_filename()
# Create the commit message file
fh = open(tmpfile, 'w')
core.write_nointr(fh, msg)
fh.close()
# Run 'git commit'
status, out = self.git.commit(F=tmpfile, v=True, amend=amend,
with_status=True,
with_stderr=True)
os.unlink(tmpfile)
return (status, out)
def tmp_dir(self):
# Allow TMPDIR/TMP with a fallback to /tmp
return os.environ.get('TMP', os.environ.get('TMPDIR', '/tmp'))
def tmp_file_pattern(self):
return os.path.join(self.tmp_dir(), '*.git-cola.%s.*' % os.getpid())
def tmp_filename(self, prefix=''):
basename = ((prefix+'.git-cola.%s.%s'
% (os.getpid(), time.time())))
basename = basename.replace('/', '-')
basename = basename.replace('\\', '-')
tmpdir = self.tmp_dir()
return os.path.join(tmpdir, basename)
def remote_url(self, name, action):
if action == 'push':
url = self.git.config('remote.%s.pushurl' % name, get=True)
if url:
return url
return self.git.config('remote.%s.url' % name, get=True)
def remote_args(self, remote,
local_branch='',
remote_branch='',
ffwd=True,
tags=False,
rebase=False,
push=False):
# Swap the branches in push mode (reverse of fetch)
if push:
tmp = local_branch
local_branch = remote_branch
remote_branch = tmp
if ffwd:
branch_arg = '%s:%s' % ( remote_branch, local_branch )
else:
branch_arg = '+%s:%s' % ( remote_branch, local_branch )
args = [remote]
if local_branch and remote_branch:
args.append(branch_arg)
elif local_branch:
args.append(local_branch)
elif remote_branch:
args.append(remote_branch)
kwargs = {
'verbose': True,
'tags': tags,
'rebase': rebase,
'with_stderr': True,
'with_status': True,
}
return (args, kwargs)
def gen_remote_helper(self, gitaction, push=False):
"""Generates a closure that calls git fetch, push or pull
"""
def remote_helper(remote, **kwargs):
args, kwargs = self.remote_args(remote, push=push, **kwargs)
return gitaction(*args, **kwargs)
return remote_helper
def create_branch(self, name, base, track=False):
"""Create a branch named 'name' from revision 'base'
Pass track=True to create a local tracking branch.
"""
return self.git.branch(name, base, track=track,
with_stderr=True,
with_status=True)
def cherry_pick_list(self, revs, **kwargs):
"""Cherry-picks each revision into the current branch.
Returns a list of command output strings (1 per cherry pick)"""
if not revs:
return []
cherries = []
status = 0
for rev in revs:
newstatus, out = self.git.cherry_pick(rev,
with_stderr=True,
with_status=True)
if status == 0:
status += newstatus
cherries.append(out)
return (status, '\n'.join(cherries))
def pad(self, pstr, num=22):
topad = num-len(pstr)
if topad > 0:
return pstr + ' '*topad
else:
return pstr
def is_commit_published(self):
head = self.git.rev_parse('HEAD')
return bool(self.git.branch(r=True, contains=head))
def everything(self):
"""Returns a sorted list of all files, including untracked files."""
ls_files = self.git.ls_files(z=True,
cached=True,
others=True,
exclude_standard=True)
return sorted(map(core.decode, [f for f in ls_files.split('\0') if f]))
def stage_paths(self, paths):
"""Stages add/removals to git."""
if not paths:
self.stage_all()
return
add = []
remove = []
for path in set(paths):
if os.path.exists(core.encode(path)):
add.append(path)
else:
remove.append(path)
self.notify_message_observers(self.message_about_to_update)
# `git add -u` doesn't work on untracked files
if add:
self._sliced_add(add)
# If a path doesn't exist then that means it should be removed
# from the index. We use `git add -u` for that.
if remove:
while remove:
self.git.add('--', u=True, with_stderr=True, *remove[:42])
remove = remove[42:]
self._update_files()
self.notify_message_observers(self.message_updated)
def unstage_paths(self, paths):
if not paths:
self.unstage_all()
return
gitcmds.unstage_paths(paths, head=self.head)
self.update_file_status()
def getcwd(self):
"""If we've chosen a directory then use it, otherwise os.getcwd()."""
if self.directory:
return self.directory
return os.getcwd()
serializer.handlers[MainModel] = MainSerializer
+556
View File
@@ -0,0 +1,556 @@
"""This view provides the main git-cola user interface.
"""
import os
from PyQt4 import QtCore
from PyQt4 import QtGui
from PyQt4.QtCore import Qt
from PyQt4.QtCore import SIGNAL
import cola
from cola import core
from cola import gitcmds
from cola import guicmds
from cola import merge
from cola import settings
from cola import signals
from cola import gitcfg
from cola import qtutils
from cola import qtcompat
from cola import qt
from cola import resources
from cola import stash
from cola import utils
from cola import version
from cola.classic import cola_classic
from cola.classic import classic_widget
from cola.controllers import compare
from cola.controllers import createtag
from cola.controllers import search
from cola.controllers.bookmark import manage_bookmarks
from cola.controllers.bookmark import save_bookmark
from cola.controllers.createbranch import create_new_branch
from cola.dag import git_dag
from cola.prefs import diff_font
from cola.prefs import PreferencesModel
from cola.prefs import preferences
from cola.qt import create_button
from cola.qt import create_dock
from cola.qt import create_menu
from cola.qtutils import add_action
from cola.qtutils import connect_button
from cola.qtutils import emit
from cola.qtutils import log
from cola.qtutils import relay_signal
from cola.qtutils import SLOT
from cola.qtutils import tr
from cola.views import standard
from cola.widgets import cfgactions
from cola.widgets.about import launch_about_dialog
from cola.widgets.commitmsg import CommitMessageEditor
from cola.widgets.diff import DiffTextEdit
from cola.widgets.status import StatusWidget
class MainView(standard.MainWindow):
def __init__(self, model, parent=None,
add_action=add_action,
connect_button=connect_button,
create_button=create_button,
create_dock=create_dock,
create_menu=create_menu,
emit=emit,
relay_signal=relay_signal,
SLOT=SLOT,
tr=tr):
standard.MainWindow.__init__(self, parent)
# Default size; this is thrown out when save/restore is used
self.resize(987, 610)
self.model = model
self.prefs_model = prefs_model = PreferencesModel()
# Internal field used by import/export_state().
# Change this whenever dockwidgets are removed.
self.widget_version = 1
# Keeps track of merge messages we've seen
self.merge_message_hash = ''
self.setAcceptDrops(True)
# Dockwidget options
qtcompat.set_common_dock_options(self)
self.classic_dockable = gitcfg.instance().get('cola.classicdockable')
if self.classic_dockable:
self.classicdockwidget = create_dock('Cola Classic', self)
self.classicwidget = classic_widget(parent=self)
self.classicdockwidget.setWidget(self.classicwidget)
# "Actions" widget
self.actiondockwidget = create_dock('Actions', self)
self.actiondockwidgetcontents = qt.QFlowLayoutWidget(parent=self)
layout = self.actiondockwidgetcontents.layout()
self.stage_button = create_button('Stage', layout)
self.unstage_button = create_button('Unstage', layout)
self.rescan_button = create_button('Rescan', layout)
self.fetch_button = create_button('Fetch...', layout)
self.push_button = create_button('Push...', layout)
self.pull_button = create_button('Pull...', layout)
self.stash_button = create_button('Stash...', layout)
self.alt_button = create_button('Exit Diff Mode', layout)
self.alt_button.hide()
layout.addStretch()
self.actiondockwidget.setWidget(self.actiondockwidgetcontents)
# "Repository Status" widget
self.statusdockwidget = create_dock('Repository Status', self)
self.statusdockwidget.setWidget(StatusWidget(self))
# "Commit Message Editor" widget
self.commitdockwidget = create_dock('Commit Message Editor', self)
self.commitmsgeditor = CommitMessageEditor(model, self)
relay_signal(self, self.commitmsgeditor, SIGNAL(signals.amend_mode))
relay_signal(self, self.commitmsgeditor, SIGNAL(signals.signoff))
relay_signal(self, self.commitmsgeditor,
SIGNAL(signals.load_previous_message))
self.commitdockwidget.setWidget(self.commitmsgeditor)
# "Command Output" widget
logwidget = qtutils.logger()
logwidget.setFont(diff_font())
self.logdockwidget = create_dock('Command Output', self)
self.logdockwidget.setWidget(logwidget)
# "Diff Viewer" widget
self.diffdockwidget = create_dock('Diff Viewer', self)
self.diff_viewer = DiffTextEdit(self.diffdockwidget)
self.diffdockwidget.setWidget(self.diff_viewer)
# All Actions
self.menu_unstage_selected = add_action(self,
'Unstage From Commit', emit(self, signals.unstage_selected))
self.menu_show_diffstat = add_action(self,
'Diffstat', emit(self, signals.diffstat), 'Ctrl+D')
self.menu_stage_modified = add_action(self,
'Stage Changed Files To Commit',
emit(self, signals.stage_modified), 'Alt+A')
self.menu_stage_untracked = add_action(self,
'Stage All Untracked', emit(self, signals.stage_untracked), 'Alt+U')
self.menu_export_patches = add_action(self,
'Export Patches...', guicmds.export_patches, 'Ctrl+E')
self.menu_preferences = add_action(self,
'Preferences', lambda: preferences(model=prefs_model),
QtGui.QKeySequence.Preferences, 'Ctrl+O')
self.menu_rescan = add_action(self,
'Rescan', emit(self, signals.rescan), 'Ctrl+R')
self.menu_cherry_pick = add_action(self,
'Cherry-Pick...', guicmds.cherry_pick, 'Ctrl+P')
self.menu_unstage_all = add_action(self,
'Unstage All', emit(self, signals.unstage_all))
self.menu_load_commitmsg = add_action(self,
'Load Commit Message...', guicmds.load_commitmsg_slot(self))
self.menu_quit = add_action(self,
'Quit', self.close, 'Ctrl+Q')
self.menu_manage_bookmarks = add_action(self,
'Bookmarks...', manage_bookmarks)
self.menu_save_bookmark = add_action(self,
'Bookmark Current...', save_bookmark)
self.menu_grep = add_action(self,
'Grep', guicmds.grep)
self.menu_merge_local = add_action(self,
'Merge...', merge.local_merge)
self.menu_merge_abort = add_action(self,
'Abort Merge...', merge.abort_merge)
self.menu_fetch = add_action(self,
'Fetch...', guicmds.fetch_slot(self))
self.menu_push = add_action(self,
'Push...', guicmds.push_slot(self))
self.menu_pull = add_action(self,
'Pull...', guicmds.pull_slot(self))
self.menu_open_repo = add_action(self,
'Open...', guicmds.open_repo_slot(self))
self.menu_stash = add_action(self,
'Stash...', lambda: stash.stash(parent=self), 'Alt+Shift+S')
self.menu_diff_branch = add_action(self,
'Apply Changes From Branch...', guicmds.diff_branch)
self.menu_branch_compare = add_action(self,
'Branches...', compare.branch_compare)
self.menu_clone_repo = add_action(self,
'Clone...', guicmds.clone_repo)
self.menu_help_docs = add_action(self,
'Documentation',
lambda: self.model.git.web__browse(resources.html_docs()),
QtGui.QKeySequence.HelpContents)
self.menu_commit_compare = add_action(self,
'Commits...', compare.compare)
self.menu_commit_compare_file = add_action(self,
'Commits Touching File...', compare.compare_file)
self.menu_visualize_current = add_action(self,
'Visualize Current Branch...',
emit(self, signals.visualize_current))
self.menu_visualize_all = add_action(self,
'Visualize All Branches...',
emit(self, signals.visualize_all))
self.menu_search_commits = add_action(self,
'Search...', search.search)
self.menu_browse_branch = add_action(self,
'Browse Current Branch...', guicmds.browse_current)
self.menu_browse_other_branch = add_action(self,
'Browse Other Branch...', guicmds.browse_other)
self.menu_load_commitmsg_template = add_action(self,
'Get Commit Message Template',
emit(self, signals.load_commit_template))
self.menu_help_about = add_action(self,
'About', launch_about_dialog)
self.menu_branch_diff = add_action(self,
'SHA-1...', guicmds.branch_diff)
self.menu_diff_expression = add_action(self,
'Expression...', guicmds.diff_expression)
self.menu_create_tag = add_action(self,
'Create Tag...', createtag.create_tag)
self.menu_create_branch = add_action(self,
'Create...', create_new_branch, 'Ctrl+B')
self.menu_delete_branch = add_action(self,
'Delete...', guicmds.branch_delete)
self.menu_checkout_branch = add_action(self,
'Checkout...', guicmds.checkout_branch, 'Alt+B')
self.menu_rebase_branch = add_action(self,
'Rebase...', guicmds.rebase)
self.menu_branch_review = add_action(self,
'Review...', guicmds.review_branch)
self.menu_classic = add_action(self,
'Cola Classic...', cola_classic)
self.menu_dag = add_action(self,
'DAG...', lambda: git_dag(self.model, self))
# Create the application menu
self.menubar = QtGui.QMenuBar(self)
# File Menu
self.file_menu = create_menu('&File', self.menubar)
self.file_menu.addAction(self.menu_preferences)
self.file_menu.addSeparator()
self.file_menu.addAction(self.menu_open_repo)
self.file_menu.addAction(self.menu_clone_repo)
self.file_menu.addSeparator()
self.file_menu.addAction(self.menu_rescan)
self.file_menu.addSeparator()
self.file_menu.addAction(self.menu_manage_bookmarks)
self.file_menu.addAction(self.menu_save_bookmark)
self.file_menu.addSeparator()
self.file_menu.addAction(self.menu_load_commitmsg)
self.file_menu.addAction(self.menu_load_commitmsg_template)
self.file_menu.addSeparator()
self.file_menu.addAction(self.menu_quit)
# Add to menubar
self.menubar.addAction(self.file_menu.menuAction())
# Commit Menu
self.commit_menu = create_menu('Co&mmit', self.menubar)
self.commit_menu.setTitle(tr('Commit@@verb'))
self.commit_menu.addAction(self.menu_stage_modified)
self.commit_menu.addAction(self.menu_stage_untracked)
self.commit_menu.addSeparator()
self.commit_menu.addAction(self.menu_unstage_all)
self.commit_menu.addAction(self.menu_unstage_selected)
self.commit_menu.addSeparator()
self.commit_menu.addAction(self.menu_search_commits)
# Add to menubar
self.menubar.addAction(self.commit_menu.menuAction())
# Branch Menu
self.branch_menu = create_menu('B&ranch', self.menubar)
self.branch_menu.addAction(self.menu_branch_review)
self.branch_menu.addSeparator()
self.branch_menu.addAction(self.menu_create_branch)
self.branch_menu.addAction(self.menu_checkout_branch)
self.branch_menu.addAction(self.menu_rebase_branch)
self.branch_menu.addAction(self.menu_delete_branch)
self.branch_menu.addSeparator()
self.branch_menu.addAction(self.menu_browse_branch)
self.branch_menu.addAction(self.menu_browse_other_branch)
self.branch_menu.addSeparator()
self.branch_menu.addAction(self.menu_visualize_current)
self.branch_menu.addAction(self.menu_visualize_all)
self.branch_menu.addSeparator()
self.branch_menu.addAction(self.menu_diff_branch)
# Add to menubar
self.menubar.addAction(self.branch_menu.menuAction())
# Actions menu
self.actions_menu = create_menu('Act&ions', self.menubar)
self.actions_menu.addAction(self.menu_merge_local)
self.actions_menu.addAction(self.menu_stash)
self.actions_menu.addSeparator()
self.actions_menu.addAction(self.menu_fetch)
self.actions_menu.addAction(self.menu_push)
self.actions_menu.addAction(self.menu_pull)
self.actions_menu.addSeparator()
self.actions_menu.addAction(self.menu_create_tag)
self.actions_menu.addSeparator()
self.actions_menu.addAction(self.menu_export_patches)
self.actions_menu.addAction(self.menu_cherry_pick)
self.actions_menu.addSeparator()
self.actions_menu.addAction(self.menu_merge_abort)
self.actions_menu.addAction(self.menu_grep)
# Add to menubar
self.menubar.addAction(self.actions_menu.menuAction())
# Diff Menu
self.diff_menu = create_menu('&Diff', self.menubar)
self.diff_menu.addAction(self.menu_branch_diff)
self.diff_menu.addAction(self.menu_diff_expression)
self.diff_menu.addSeparator()
self.diff_menu.addAction(self.menu_branch_compare)
self.diff_menu.addAction(self.menu_commit_compare)
self.diff_menu.addAction(self.menu_commit_compare_file)
self.diff_menu.addSeparator()
self.diff_menu.addAction(self.menu_show_diffstat)
# Add to menubar
self.menubar.addAction(self.diff_menu.menuAction())
# Tools Menu
self.tools_menu = create_menu('&Tools', self.menubar)
self.tools_menu.addAction(self.menu_classic)
self.tools_menu.addAction(self.menu_dag)
self.tools_menu.addSeparator()
if self.classic_dockable:
self.tools_menu.addAction(self.classicdockwidget.toggleViewAction())
self.tools_menu.addAction(self.diffdockwidget.toggleViewAction())
self.tools_menu.addAction(self.actiondockwidget.toggleViewAction())
self.tools_menu.addAction(self.commitdockwidget.toggleViewAction())
self.tools_menu.addAction(self.statusdockwidget.toggleViewAction())
self.tools_menu.addAction(self.logdockwidget.toggleViewAction())
self.menubar.addAction(self.tools_menu.menuAction())
# Help Menu
self.help_menu = create_menu('&Help', self.menubar)
self.help_menu.addAction(self.menu_help_docs)
self.help_menu.addAction(self.menu_help_about)
# Add to menubar
self.menubar.addAction(self.help_menu.menuAction())
# Set main menu
self.setMenuBar(self.menubar)
# Arrange dock widgets
top = Qt.TopDockWidgetArea
bottom = Qt.BottomDockWidgetArea
self.addDockWidget(top, self.commitdockwidget)
if self.classic_dockable:
self.addDockWidget(top, self.classicdockwidget)
self.addDockWidget(top, self.statusdockwidget)
self.addDockWidget(top, self.actiondockwidget)
self.addDockWidget(bottom, self.logdockwidget)
if self.classic_dockable:
self.tabifyDockWidget(self.classicdockwidget, self.commitdockwidget)
self.tabifyDockWidget(self.logdockwidget, self.diffdockwidget)
# Listen for model notifications
model.add_message_observer(model.message_mode_changed,
self._mode_changed)
model.add_message_observer(model.message_updated,
self._update_view)
prefs_model.add_message_observer(prefs_model.message_config_updated,
self._config_updated)
# Add button callbacks
connect_button(self.rescan_button, emit(self, signals.rescan))
connect_button(self.alt_button, emit(self, signals.reset_mode))
connect_button(self.fetch_button, guicmds.fetch_slot(self))
connect_button(self.push_button, guicmds.push_slot(self))
connect_button(self.pull_button, guicmds.pull_slot(self))
connect_button(self.stash_button, lambda: stash.stash(parent=self))
connect_button(self.stage_button, self.stage)
connect_button(self.unstage_button, self.unstage)
self.connect(self, SIGNAL('update'), self._update_callback)
self.connect(self, SIGNAL('apply_state'), self.apply_state)
self.connect(self, SIGNAL('install_config_actions'),
self._install_config_actions)
# Install .git-config-defined actions
self._config_task = None
self.install_config_actions()
# Restore saved settings
self._gui_state_task = None
self._load_gui_state()
log(0, self.model.git_version + '\ncola version ' + version.version())
# Qt overrides
def closeEvent(self, event):
"""Save state in the settings manager."""
qtutils.save_state(self)
standard.MainWindow.closeEvent(self, event)
# Accessors
mode = property(lambda self: self.model.mode)
def _mode_changed(self, mode):
"""React to mode changes; hide/show the "Exit Diff Mode" button."""
if mode in (self.model.mode_branch,
self.model.mode_review,
self.model.mode_diff,
self.model.mode_diff_expr):
height = self.stage_button.minimumHeight()
self.alt_button.setMinimumHeight(height)
self.alt_button.show()
else:
self.alt_button.setMinimumHeight(1)
self.alt_button.hide()
def _config_updated(self, source, config, value):
if config == 'cola.fontdiff':
font = QtGui.QFont()
if not font.fromString(value):
return
self._set_diff_font(font)
qtutils.logger().setFont(font)
self.diff_viewer.setFont(font)
self.commitmsgeditor.commitmsg.setFont(font)
elif config == 'cola.tabwidth':
# variable-tab-width setting
self.diff_viewer.set_tab_width(value)
def install_config_actions(self):
"""Install .gitconfig-defined actions"""
self._config_task = self._start_config_actions_task()
def _start_config_actions_task(self):
"""Do the expensive "get_config_actions()" call in the background"""
class ConfigActionsTask(QtCore.QRunnable):
def __init__(self, sender):
QtCore.QRunnable.__init__(self)
self._sender = sender
def run(self):
names = cfgactions.get_config_actions()
self._sender.emit(SIGNAL('install_config_actions'), names)
task = ConfigActionsTask(self)
QtCore.QThreadPool.globalInstance().start(task)
return task
def _install_config_actions(self, names):
"""Install .gitconfig-defined actions"""
if not names:
return
menu = self.actions_menu
menu.addSeparator()
for name in names:
menu.addAction(name, emit(self, signals.run_config_action, name))
def _update_view(self):
self.emit(SIGNAL('update'))
def _update_callback(self):
"""Update the title with the current branch and directory name."""
branch = self.model.currentbranch
curdir = core.decode(os.getcwd())
msg = 'Repository: %s\nBranch: %s' % (curdir, branch)
self.commitdockwidget.setToolTip(msg)
title = '%s: %s' % (self.model.project, branch)
if self.mode in (self.model.mode_diff, self.model.mode_diff_expr):
title += ' *** diff mode***'
elif self.mode == self.model.mode_review:
title += ' *** review mode***'
elif self.mode == self.model.mode_amend:
title += ' *** amending ***'
self.setWindowTitle(title)
self.commitmsgeditor.set_mode(self.mode)
if not self.model.read_only() and self.mode != self.model.mode_amend:
# Check if there's a message file in .git/
merge_msg_path = gitcmds.merge_message_path()
if merge_msg_path is None:
return
merge_msg_hash = utils.checksum(core.decode(merge_msg_path))
if merge_msg_hash == self.merge_message_hash:
return
self.merge_message_hash = merge_msg_hash
cola.notifier().broadcast(signals.load_commit_message,
core.decode(merge_msg_path))
def apply_state(self, state):
"""Imports data for save/restore"""
# 1 is the widget version; change when widgets are added/removed
standard.MainWindow.apply_state(self, state)
qtutils.apply_window_state(self, state, 1)
def export_state(self):
"""Exports data for save/restore"""
state = standard.MainWindow.export_state(self)
return qtutils.export_window_state(self, state, self.widget_version)
def _load_gui_state(self):
"""Restores the gui from the preferences file."""
self._gui_state_task = self._start_gui_state_loading_thread()
def _start_gui_state_loading_thread(self):
"""Do expensive file reading and json decoding in the background"""
class LoadGUIStateTask(QtCore.QRunnable):
def __init__(self, sender):
QtCore.QRunnable.__init__(self)
self._sender = sender
def run(self):
state = settings.SettingsManager.gui_state(self._sender)
self._sender.emit(SIGNAL('apply_state'), state)
task = LoadGUIStateTask(self)
QtCore.QThreadPool.globalInstance().start(task)
return task
def stage(self):
"""Stage selected files, or all files if no selection exists."""
paths = cola.selection_model().unstaged
if not paths:
cola.notifier().broadcast(signals.stage_modified)
else:
cola.notifier().broadcast(signals.stage, paths)
def unstage(self):
"""Unstage selected files, or all files if no selection exists."""
paths = cola.selection_model().staged
if not paths:
cola.notifier().broadcast(signals.unstage_all)
else:
cola.notifier().broadcast(signals.unstage, paths)
def dragEnterEvent(self, event):
"""Accepts drops"""
standard.MainWindow.dragEnterEvent(self, event)
event.acceptProposedAction()
def dropEvent(self, event):
"""Apply dropped patches with git-am"""
event.accept()
urls = event.mimeData().urls()
if not urls:
return
paths = map(lambda x: unicode(x.path()), urls)
patches = [p for p in paths if p.endswith('.patch')]
dirs = [p for p in paths if os.path.isdir(p)]
dirs.sort()
for d in dirs:
patches.extend(self._gather_patches(d))
# Broadcast the patches to apply
cola.notifier().broadcast(signals.apply_patches, patches)
def _gather_patches(self, path):
"""Find patches in a subdirectory"""
patches = []
for root, subdirs, files in os.walk(path):
for name in [f for f in files if f.endswith('.patch')]:
patches.append(os.path.join(root, name))
return patches
+47
View File
@@ -0,0 +1,47 @@
from PyQt4 import QtGui
from cola import gitcmds
from cola import qtutils
from cola.merge.controller import MergeController
from cola.merge.model import MergeModel
from cola.merge.view import MergeView
def local_merge():
"""Provides a dialog for merging branches"""
model = MergeModel()
parent = QtGui.QApplication.instance().activeWindow()
view = MergeView(model, parent)
ctl = MergeController(model, view)
view.show()
view.raise_()
return ctl
def abort_merge():
"""Prompts before aborting a merge in progress
"""
title = 'Abort Merge...'
txt = ('Abort merge?\n\n'
'Aborting the current merge will cause '
'*ALL* uncommitted changes to be lost.\n\n'
'Continue with aborting the current merge?')
info = 'Recovering uncommitted changes will not be possible'
parent = QtGui.QApplication.instance().activeWindow()
ok_text = qtutils.tr('Abort Merge...')
if ok_text.endswith(unichr(0x2026)):
ok_text = ok_text[:-1]
elif ok_text.endswith('...'):
ok_text = ok_text[:-3]
answer = qtutils.confirm(parent, title, txt, info, ok_text)
if answer:
gitcmds.abort_merge()
if __name__ == '__main__':
from cola import app
app = app.ColaApplication([])
ctl = local_merge()
ctl.model.update_status()
app.exec_()
+8
View File
@@ -0,0 +1,8 @@
"""Translate signals from the merge dialog."""
from cola.ctrl import Controller
from cola.merge.model import command_directory
class MergeController(Controller):
def __init__(self, model, view):
Controller.__init__(self, model, view)
self.add_commands(command_directory)
+70
View File
@@ -0,0 +1,70 @@
import cola
from cola import gitcmds
from cola import observable
from cola import signals
from cola.cmds import BaseCommand, VisualizeRevision, visualize_revision
class MergeModel(observable.Observable):
message_updated = 'updated'
def __init__(self):
observable.Observable.__init__(self)
self.model = cola.model()
# Relay the "updated" message
msg = self.model.message_updated
self.model.add_message_observer(msg, self.notify_updated)
def notify_updated(self):
self.notify_message_observers(self.message_updated)
def update_status(self):
self.model.update_status()
def current_branch(self):
return self.model.currentbranch
def local_branches(self):
return self.model.local_branches
def remote_branches(self):
return self.model.remote_branches
def tags(self):
return self.model.tags
def merge(self, revision, no_commit, squash):
msg = gitcmds.merge_message(revision)
status, output = self.model.git.merge('-m'+msg,
revision,
no_commit=no_commit,
squash=squash,
with_stderr=True,
with_status=True)
return status, output
# Merge command
merge = 'merge'
class Merge(BaseCommand):
def __init__(self, revision, no_commit, squash):
BaseCommand.__init__(self)
self.revision = revision
self.no_commit = no_commit
self.squash = squash
def do(self):
squash = self.squash
revision = self.revision
no_commit = self.no_commit
status, output = self.context.merge(revision, no_commit, squash)
notifier = cola.notifier()
notifier.broadcast(signals.log_cmd, status, output)
self.context.update_status()
command_directory = {
merge: Merge,
visualize_revision: VisualizeRevision,
}
+187
View File
@@ -0,0 +1,187 @@
from PyQt4 import QtGui
from PyQt4.QtCore import Qt
from PyQt4.QtCore import SIGNAL
from cola import qtutils
from cola.qt import GitRefLineEdit
from cola.qtutils import tr
from cola.merge.model import merge, visualize_revision
class MergeView(QtGui.QDialog):
"""Provides a dialog for merging branches."""
def __init__(self, model, parent=None):
QtGui.QDialog.__init__(self, parent)
self.model = model
self.setWindowModality(Qt.WindowModal)
self.resize(700, 400)
# Widgets
self.title_label = QtGui.QLabel()
self.revision_label = QtGui.QLabel()
self.revision_label.setText(tr('Revision To Merge'))
self.revision = GitRefLineEdit()
self.radio_local = QtGui.QRadioButton()
self.radio_local.setText(tr('Local Branch'))
self.radio_local.setChecked(True)
self.radio_remote = QtGui.QRadioButton()
self.radio_remote.setText(tr('Tracking Branch'))
self.radio_tag = QtGui.QRadioButton()
self.radio_tag.setText(tr('Tag'))
self.revisions = QtGui.QListWidget()
self.revisions.setAlternatingRowColors(True)
self.button_viz = QtGui.QPushButton(self)
self.button_viz.setText(tr('Visualize'))
self.checkbox_squash = QtGui.QCheckBox(self)
self.checkbox_squash.setText(tr('Squash'))
self.checkbox_commit = QtGui.QCheckBox(self)
self.checkbox_commit.setText(tr('Commit'))
self.checkbox_commit.setChecked(True)
self.checkbox_commit_state = True
self.button_cancel = QtGui.QPushButton(self)
self.button_cancel.setText(tr('Cancel'))
self.button_merge = QtGui.QPushButton(self)
self.button_merge.setText(tr('Merge'))
# Layouts
self.revlayt = QtGui.QHBoxLayout()
self.revlayt.addWidget(self.revision_label)
self.revlayt.addWidget(self.revision)
self.revlayt.addStretch()
self.revlayt.addWidget(self.title_label)
self.radiolayt = QtGui.QHBoxLayout()
self.radiolayt.addWidget(self.radio_local)
self.radiolayt.addWidget(self.radio_remote)
self.radiolayt.addWidget(self.radio_tag)
self.buttonlayt = QtGui.QHBoxLayout()
self.buttonlayt.addWidget(self.button_viz)
self.buttonlayt.addStretch()
self.buttonlayt.addWidget(self.checkbox_squash)
self.buttonlayt.addWidget(self.checkbox_commit)
self.buttonlayt.addWidget(self.button_cancel)
self.buttonlayt.addWidget(self.button_merge)
self.mainlayt = QtGui.QVBoxLayout()
self.mainlayt.setMargin(4)
self.mainlayt.addLayout(self.radiolayt)
self.mainlayt.addWidget(self.revisions)
self.mainlayt.addLayout(self.revlayt)
self.mainlayt.addLayout(self.buttonlayt)
self.setLayout(self.mainlayt)
self.revision.setFocus()
# Signal/slot connections
self.connect(self.button_cancel, SIGNAL('clicked()'),
self.reject)
self.connect(self.checkbox_squash, SIGNAL('clicked()'),
self.toggle_squash)
self.connect(self.revision, SIGNAL('textChanged(QString)'),
self.update_title)
self.connect(self.revisions, SIGNAL('itemSelectionChanged()'),
self.revision_selected)
self.connect(self.radio_local, SIGNAL('clicked()'),
self.update_revisions)
self.connect(self.radio_remote, SIGNAL('clicked()'),
self.update_revisions)
self.connect(self.radio_tag, SIGNAL('clicked()'),
self.update_revisions)
self.connect(self.button_merge, SIGNAL('clicked()'),
self.merge_revision)
self.connect(self.button_viz, SIGNAL('clicked()'),
self.viz_revision)
# Observer messages
model.add_message_observer(model.message_updated, self.update_all)
self.update_all()
def update_all(self):
"""Set the branch name for the window title and label."""
self.update_title()
self.update_revisions()
def update_title(self, dummy_txt=None):
branch = self.model.current_branch()
revision = unicode(self.revision.text())
if revision:
txt = unicode(tr('Merge "%s" into "%s"')) % (revision, branch)
else:
txt = unicode(tr('Merge into "%s"')) % branch
self.title_label.setText(txt)
self.setWindowTitle(txt)
def toggle_squash(self):
"""Toggles the commit checkbox based on the squash checkbox."""
if self.checkbox_squash.isChecked():
self.checkbox_commit_state =\
self.checkbox_commit.checkState()
self.checkbox_commit.setCheckState(Qt.Unchecked)
self.checkbox_commit.setDisabled(True)
else:
self.checkbox_commit.setDisabled(False)
oldstate = self.checkbox_commit_state
self.checkbox_commit.setCheckState(oldstate)
def update_revisions(self):
"""Update the revision list whenever a radio button is clicked"""
self.revisions.clear()
self.revisions.addItems(self.current_revisions())
def revision_selected(self):
"""Update the revision field when a list item is selected"""
revlist = self.current_revisions()
widget = self.revisions
row, selected = qtutils.selected_row(widget)
if selected and row < len(revlist):
revision = revlist[row]
self.revision.setText(revision)
def current_revisions(self):
"""Retrieve candidate items to merge"""
if self.radio_local.isChecked():
return self.model.local_branches()
elif self.radio_remote.isChecked():
return self.model.remote_branches()
elif self.radio_tag.isChecked():
return self.model.tags()
return []
def viz_revision(self):
"""Launch a gitk-like viewer on the selection revision"""
revision = unicode(self.revision.text())
if not revision:
qtutils.information('No Revision Specified',
'You must specify a revision to view')
return
self.emit(SIGNAL(visualize_revision), revision)
def merge_revision(self):
"""Merge the selected revision/branch"""
revision = unicode(self.revision.text())
if not revision:
qtutils.information('No Revision Specified',
'You must specify a revision to merge')
return
do_commit = self.checkbox_commit.isChecked()
squash = self.checkbox_squash.isChecked()
self.emit(SIGNAL(merge), revision, not(do_commit), squash)
self.accept()
-421
View File
@@ -1,421 +0,0 @@
# Copyright (c) 2009 David Aguilar
"""
This module provides the BaseModel class, a generic, serializable
data container.
"""
import os
import imp
import types
from cStringIO import StringIO
import jsonpickle
from cola import core
class BaseModel(object):
"""
BaseModel is a base class that implements convenient
serialization methods.
"""
def __getitem__(self, item):
"""Adds support for retrieving a parameter via model['param'].
>>> m = BaseModel()
>>> m.answer = 42
>>> m['answer']
42
"""
return self.__dict__[item]
def __setitem__(self, item, value):
"""
Adds support for setting a parameter via model['param'] = value.
>>> m = BaseModel()
>>> m['answer'] = 42
>>> m.answer
42
"""
self.__dict__[item] = value
def __iter__(self):
"""Provides iterator access to a model class.
This allows you to iterate over a model's parameters as if it
were a list.
>>> m = BaseModel()
>>> m.answer = 42
>>> key = 'nada'
>>> for k, v in m: key = k
>>> key
'answer'
>>> m = BaseModel()
>>> m.answer = 42
>>> value = 'nada'
>>> for k, v in m: value = v
>>> value
42
"""
return BaseModelIterator(self)
def _filter_dict(self, dct):
"""This method removes all items that begin with an underscore.
>>> m = BaseModel()
>>> m._filter_dict({'a': 1, '_b': 2, 'c_': 3})
{'a': 1}
"""
# Create a set of (key, value) pairs
included = set([(k, v) for k, v in dct.iteritems()
if not k.startswith('_') and
not k.endswith('_') and
not k.startswith('py/')])
# Anything left over passed the filter
filtered = {}
for k, v in included:
filtered[k] = v
return filtered
def items(self, raw=False):
"""Provides a dictionary-like items() iterator.
>>> m = BaseModel().from_dict(dict(a='a', b='b'))
>>> value = 'nada'
>>> for k, v in m.items(): value = v;
>>> value
'b'
"""
if raw:
return self.to_dict().items()
return self._filter_dict(self.to_dict()).items()
def iteritems(self, raw=False):
"""Provides a dictionary-like iteritems() iterator.
>>> m = BaseModel().from_dict(dict(a='a', b='b'))
>>> value = 'nada'
>>> for k, v in m.iteritems(): value = v;
>>> value
'b'
"""
if raw:
return self.to_dict().iteritems()
return self._filter_dict(self.to_dict()).iteritems()
def param_names(self, export=False):
"""Returns a list of serializable attribute names.
>>> m = BaseModel()
>>> m._question = 'unknown'
>>> m.answer = 42
>>> m.param_names()
['answer']
>>> m.param_names(export=True)
['_question', 'answer']
"""
names = []
for k, v in self.__dict__.iteritems():
if not export and (k.startswith('_') or k.endswith('_') or
k.startswith('py/')):
continue
if is_function(v):
continue
if is_atom(v) or is_list(v) or is_dict(v) or is_model(v):
names.append(k)
names.sort()
return names
def clone(self):
"""Creates a clone of the current object.
>>> m = BaseModel()
>>> m.answer = 42
>>> clone = m.clone()
>>> clone.answer
42
>>> type(clone)
<class 'cola.models.base.BaseModel'>
"""
# Go in and out of jsonpickle to create a clone
return jsonpickle.decode(jsonpickle.encode(self))
def has_param(self,param):
"""Returns true if a parameter exists in a model.
>>> m = BaseModel()
>>> m.answer = 42
>>> m.has_param('answer')
True
>>> m.has_param('question')
False
"""
return param in self.__dict__
def param(self, param, default=None):
"""Returns the value of a model parameter.
>>> m = BaseModel()
>>> m.answer = 42
>>> m.param('answer')
42
>>> m.param('another answer', 42)
42
"""
return self.__dict__.get(param, default)
def __getattr__(self, param):
"""Provides set_<attribute>(value) append methods.
This provides automatic convenience methods for handling
setattrs with notification.
>>> m = BaseModel()
>>> m.answer = 42
>>> m.set_answer(41)
>>> # Observers are notified
>>> m.answer
41
"""
# Base case: we actually have this param
if param in self.__dict__:
return getattr(self, param)
# Return a closure over param for calling set_param;
# Concrete classes subclass set_param to provide notification
if param.startswith('set_'):
return lambda v: self.set_param(param[4:], v)
errmsg = ("'%s' object has no attribute '%s'" %
(self.__class__.__name__, param))
raise AttributeError(errmsg)
def set_param(self, param, value):
"""Wrapper around setattr()
>>> m = BaseModel()
>>> m.answer = 41
>>> m.set_param('answer', 42)
>>> m.answer
42
"""
setattr(self, param, value)
def copy_params(self, model, params_to_copy=None):
"""Copies params from one model to another.
>>> m = BaseModel()
>>> m.answer = 42
>>> m._question = 'unknown'
>>> m2 = BaseModel()
>>> m2.copy_params(m)
>>> m2._question
'unknown'
>>> m2.answer
42
"""
# Loop over all attributes and copy them over
for k in params_to_copy or model.param_names(export=True):
self[k] = model.param(k)
def save(self, filename):
"""Saves a model to a file.
"""
fh = open(filename, 'w')
jsonpickle.set_encoder_options('simplejson', indent=4)
try:
core.write_nointr(fh, jsonpickle.encode(self))
except:
pass
fh.close()
jsonpickle.set_encoder_options('simplejson', indent=None)
def load(self, filename):
"""Loads model state from a file.
"""
fh = open(filename, 'r')
contents = core.read_nointr(fh)
fh.close()
try:
ddict = jsonpickle.decode(contents)
return self.from_dict(ddict)
except:
pass
return self
@staticmethod
def instance(filename):
"""Instances a model from a filename.
"""
fh = open(filename, 'r')
contents = core.read_nointr(fh)
fh.close()
obj = jsonpickle.decode(contents)
if is_dict(obj):
return BaseModel().from_dict(obj)
else:
return obj
def from_dict(self, source_dict):
"""Import a complex model from a dictionary.
If it looks like a duck, it's a duck.
>>> BaseModel().from_dict({'answer': 42}).answer
42
"""
# Load parameters in-place
unpickler = jsonpickle.unpickler.Unpickler()
obj = unpickler.restore(source_dict)
if is_model(obj):
self.copy_params(obj)
elif is_dict(obj):
for k, v in obj.iteritems():
self[k] = unpickler.restore(v)
return self
def to_dict(self):
"""
Exports a model to a dictionary.
This simplifies serialization.
>>> is_dict(BaseModel().to_dict())
True
"""
return jsonpickle.pickler.Pickler().flatten(self)
__indent__ = 0
__preindent__ = True
__strstack__ = set()
@staticmethod
def _indent(i=0):
BaseModel.__indent__ += i
return ' ' * BaseModel.__indent__
def __str__(self):
"""A convenient, recursively-defined stringification method."""
# This avoid infinite recursion on cyclical structures
if self in BaseModel.__strstack__:
return '__self__'
else:
BaseModel.__strstack__.add(self)
# IO object for output
io = StringIO()
# Handle indentation
if BaseModel.__preindent__:
io.write(BaseModel._indent())
# Class name and opening parenthesis
io.write(self.__class__.__name__ + '(')
# Go one level deeper
BaseModel._indent(1)
# Output each attribute
for param in self.param_names():
if param.startswith('_') or param.endswith('_'):
continue
# Go to the next line
io.write('\n')
# e.g. foo = bar
inner = BaseModel._indent() + param + " = "
value = self[param]
# Lists use a new line for each item
if type(value) == types.ListType:
indent = BaseModel._indent(1)
io.write(inner + "[\n")
for val in value:
# Nested models need special treatment
if is_model(val):
io.write(str(val)+'\n')
else:
io.write(indent)
io.write(str(val))
io.write(",\n")
# Unindent, closing bracket
io.write(BaseModel._indent(-1))
io.write("],")
else:
# It's not a list, so just output its str() representation
BaseModel.__preindent__ = False
io.write(inner)
io.write(str(value))
io.write(',')
BaseModel.__preindent__ = True
# Finish this item, closing parenthesis
io.write('\n' + BaseModel._indent(-1) + ')')
value = io.getvalue()
io.close()
# Remove ourselves from the recursion-avoidance stack
BaseModel.__strstack__.remove(self)
return value
#############################################################################
class BaseModelIterator(object):
"""Provides an iterator over model (key, value) pairs.
"""
def __init__(self, model):
self.model = model
self.params = model.param_names()
self.idx = -1
def next(self):
try:
self.idx += 1
name = self.params[self.idx]
return (name, self.model[name])
except IndexError:
raise StopIteration
#############################################################################
def is_model(item):
"""Is this an instance of a BaseModel-like object?"""
return isinstance(item, BaseModel)
def is_dict(item):
"""Is this an instance of a dictionary?"""
return type(item) is types.DictType
def is_list(item):
"""Is this a list?"""
return type(item) is types.ListType or type(item) is types.TupleType
def is_atom(item):
"""Is this an atom?"""
return(type(item) in types.StringTypes
or type(item) is types.BooleanType
or type(item) is types.IntType
or type(item) is types.LongType
or type(item) is types.FloatType
or type(item) is types.ComplexType)
def is_function(item):
"""Is this a function?"""
return type(item) is types.FunctionType
+94
View File
@@ -0,0 +1,94 @@
import re
import cola
from cola import gitcmds
from cola.obsmodel import ObservableModel
class BrowserModel(ObservableModel):
def __init__(self, branch=None):
ObservableModel.__init__(self)
self.copy_params(cola.model())
if branch:
self.currentbranch = branch
# These are parallel lists
# ref^{tree}
self.types = []
self.sha1s = []
self.names = []
self.directories = []
self.directory_entries = {}
# parallel lists
self.subtree_types = []
self.subtree_sha1s = []
self.subtree_names = []
def init_browser_data(self):
"""This scans over self.(names, sha1s, types) to generate
directories, directory_entries, and subtree_*"""
# Collect data for the model
if not self.currentbranch:
return
self.subtree_types = []
self.subtree_sha1s = []
self.subtree_names = []
self.directories = []
self.directory_entries = {}
# Lookup the tree info
tree_info = gitcmds.parse_ls_tree(self.currentbranch)
self.set_types(map(lambda(x): x[1], tree_info ))
self.set_sha1s(map(lambda(x): x[2], tree_info ))
self.set_names(map(lambda(x): x[3], tree_info ))
if self.directory:
self.directories.append('..')
dir_entries = self.directory_entries
dir_regex = re.compile('([^/]+)/')
dirs_seen = {}
subdirs_seen = {}
for idx, name in enumerate(self.names):
if not name.startswith(self.directory):
continue
name = name[ len(self.directory): ]
if name.count('/'):
# This is a directory...
match = dir_regex.match(name)
if not match:
continue
dirent = match.group(1) + '/'
if dirent not in self.directory_entries:
self.directory_entries[dirent] = []
if dirent not in dirs_seen:
dirs_seen[dirent] = True
self.directories.append(dirent)
entry = name.replace(dirent, '')
entry_match = dir_regex.match(entry)
if entry_match:
subdir = entry_match.group(1) + '/'
if subdir in subdirs_seen:
continue
subdirs_seen[subdir] = True
dir_entries[dirent].append(subdir)
else:
dir_entries[dirent].append(entry)
else:
self.subtree_types.append(self.types[idx])
self.subtree_sha1s.append(self.sha1s[idx])
self.subtree_names.append(name)
def subtree_node(self, idx):
"""Return data for the tree item at idx."""
return (self.subtree_types[idx],
self.subtree_sha1s[idx],
self.subtree_names[idx])
+48 -9
View File
@@ -1,12 +1,15 @@
import os
from cola import core
from cola import git
from cola import gitcmds
from cola.obsmodel import ObservableModel
import cola
from cola.models.main import MainModel
class CompareModel(MainModel):
class CompareModel(ObservableModel):
"""Provides custom model data for CompareController."""
def __init__(self):
MainModel.__init__(self)
ObservableModel.__init__(self)
self.git = git.instance()
self.descriptions_start = []
self.descriptions_end = []
self.revisions_start = []
@@ -16,12 +19,49 @@ class CompareModel(MainModel):
self.compare_files = []
self.num_results = 100
self.show_versions=False
self.copy_params(cola.model())
class BranchCompareModel(MainModel):
def update_revision_lists(self, filename=None, show_versions=False):
num_results = self.num_results
if filename:
rev_list = self.git.log('--', filename,
max_count=num_results,
no_color=True,
pretty='oneline')
else:
rev_list = self.git.log(max_count=num_results,
no_color=True,
pretty='oneline', all=True)
commit_list = gitcmds.parse_rev_list(rev_list)
commit_list.reverse()
commits = map(lambda x: x[0], commit_list)
descriptions = map(lambda x: core.decode(x[1]), commit_list)
if show_versions:
fancy_descr_list = map(lambda x: self.describe(*x), commit_list)
self.set_descriptions_start(fancy_descr_list)
self.set_descriptions_end(fancy_descr_list)
else:
self.set_descriptions_start(descriptions)
self.set_descriptions_end(descriptions)
self.set_revisions_start(commits)
self.set_revisions_end(commits)
return commits
def describe(self, revid, descr):
version = self.git.describe(revid, tags=True, always=True,
abbrev=4)
return version + ' - ' + descr
class BranchCompareModel(ObservableModel):
"""Provides custom model data for BranchCompareController."""
def __init__(self):
MainModel.__init__(self)
ObservableModel.__init__(self)
self.git = git.instance()
self.remote_branches = gitcmds.branch_list(remote=True)
self.local_branches = gitcmds.branch_list(remote=False)
self.left_combo = ['Local', 'Remote']
self.right_combo = ['Local', 'Remote']
self.left_combo_index = 0
@@ -33,4 +73,3 @@ class BranchCompareModel(MainModel):
self.left_list_selected = False
self.right_list_selected = False
self.diff_files = []
self.copy_params(cola.model())
-1233
View File
File diff suppressed because it is too large Load Diff
-67
View File
@@ -1,67 +0,0 @@
import copy
from cola.observable import Observable
from cola.models.base import BaseModel as Model
_unserializable_attributes = {
'observers': set(),
'message_observers': {},
'notification_enabled': True,
}
class ObservableModel(Model, Observable):
"""Combines serialization from Model with the Observer pattern"""
def __init__(self):
Model.__init__(self)
Observable.__init__(self)
def save(self, path):
"""
Saves state to a file. Overrides the base method.
Hides internal observable attributes when serializing.
"""
attributes = self.remove_unserializable_attributes()
Model.save(self, path)
self.restore_unserializable_attributes(attributes)
def clone(self):
"""Override Model.clone() to handle observers"""
# Go in and out of jsonpickle to create a clone
attributes = self.remove_unserializable_attributes()
clone = Model.clone(self)
unserializable_copy = copy.deepcopy(_unserializable_attributes)
self.restore_unserializable_attributes(attributes)
clone.restore_unserializable_attributes(unserializable_copy)
return clone
def set_param(self, param, value, notify=True):
"""Override Model.set_param() to handle notification"""
Model.set_param(self, param, value)
# Perform notifications
if notify:
self.notify_observers(param)
def remove_unserializable_attributes(self):
"""Remove unserializable instance attributes."""
attributes = {}
for attribute in _unserializable_attributes:
attributes[attribute] = self.__dict__.pop(attribute)
return attributes
def restore_unserializable_attributes(self, attributes):
"""Restores unserializable instance attributes."""
# Restore properties
self.__dict__.update(attributes)
@staticmethod
def instance(path):
"""Override Model.instance() to account for unserializable data."""
obj = Model.instance(path)
if isinstance(obj, ObservableModel):
# This ensures that clones always start out with no observers,
# but retain the hidden _unserializable_attributes.
unserializable_copy = copy.deepcopy(_unserializable_attributes)
obj.restore_unserializable_attributes(unserializable_copy)
return obj
+3 -3
View File
@@ -1,8 +1,8 @@
from cola.models import main
from cola.main.model import MainModel
class SearchModel(main.MainModel):
class SearchModel(MainModel):
def __init__(self, cwd=None):
main.MainModel.__init__(self, cwd=cwd)
MainModel.__init__(self, cwd=cwd)
self.query = ''
self.max_results = 500
self.start_date = ''
+18 -7
View File
@@ -1,15 +1,13 @@
"""Provides a selection model to handle selection."""
from cola.models.observable import ObservableModel
from cola.obsmodel import ObservableModel
from cola.decorators import memoize
_selection_model = None
@memoize
def selection_model():
"""Provides access to a static SelectionModel instance."""
global _selection_model
if _selection_model:
return _selection_model
_selection_model = SelectionModel()
return _selection_model
return SelectionModel()
def selection():
@@ -36,6 +34,19 @@ def single_selection():
return s, m, um, ut
def filename():
s, m, um, ut = single_selection()
if s:
return s
if m:
return m
if um:
return um
if ut:
return ut
return None
class SelectionModel(ObservableModel):
"""Provides information about selected file paths."""
# Notification message sent out when selection changes
+3 -3
View File
@@ -1,11 +1,11 @@
"""Simple data container for the CreateTag dialog."""
import cola
from cola.models import observable
from cola.obsmodel import ObservableModel
class TagModel(observable.ObservableModel):
class TagModel(ObservableModel):
def __init__(self):
observable.ObservableModel.__init__(self)
ObservableModel.__init__(self)
self.tag_msg = ''
self.tag_name = ''
self.revision = ['HEAD'] + cola.model().all_branches()
+16 -17
View File
@@ -1,37 +1,36 @@
import os
from PyQt4 import QtCore
from cola import signals
from cola.compat import set
from cola.decorators import memoize
debug = os.environ.get('COLA_NOTIFIER_DEBUG', False)
_instance = None
@memoize
def notifier():
global _instance
if _instance:
return _instance
_instance = Notifier()
return _instance
return Notifier()
class Notifier(object):
"""A pure-python re-implementation of QObject."""
"""Object for sending and receiving notification messages"""
def __init__(self):
self.channels = {}
def broadcast(self, signal, *args, **opts):
if debug:
print ('broadcast: %s(%s, %s)' % (signals.name(signal),
print ('broadcast: %s(%s, %s)' % (signal,
args or '<empty>',
kwargs or '<empty>'))
opts or '<empty>'))
self.emit(signal, *args, **opts)
def emit(self, signal, *args, **opts):
subscribers = self.channels.get(signal, None)
if subscribers:
for fxn in subscribers:
fxn(*args, **opts)
def listen(self, signal, callback):
self.connect(signal, callback)
if not subscribers:
return
for fxn in subscribers:
fxn(*args, **opts)
def connect(self, signal, callback):
subscribers = self.channels.setdefault(signal, set())
+1 -1
View File
@@ -1,7 +1,7 @@
# Copyright (c) 2008 David Aguilar
"""This module provides the Observable class"""
import types
from cola.compat import set
class Observable(object):
"""Handles subject/observer notifications."""
+48
View File
@@ -0,0 +1,48 @@
import copy
from cola import serializer
from cola.observable import Observable
from cola.basemodel import BaseModel as Model
from cola.compat import set
class ObservableModel(Model, Observable):
"""Combines serialization from Model with the Observer pattern"""
def __init__(self):
Model.__init__(self)
Observable.__init__(self)
def set_param(self, param, value, notify=True):
"""Override Model.set_param() to handle notification"""
Model.set_param(self, param, value)
# Perform notifications
if notify:
self.notify_observers(param)
_unserializable_attributes = {
'observers': set(),
'message_observers': {},
'notification_enabled': True,
}
class OMSerializer(object):
"""Hide the internal 'observers' fields from serialization"""
def __init__(self, obj):
self.obj = obj
self.attributes = {}
def pre_encode_hook(self):
for attribute in _unserializable_attributes:
self.attributes[attribute] = self.obj.__dict__.pop(attribute)
def post_encode_hook(self):
self.obj.__dict__.update(self.attributes)
self.attributes = {}
def post_decode_hook(self):
self.obj.__dict__.update(copy.deepcopy(_unserializable_attributes))
# Add a hook for this object
serializer.handlers[ObservableModel] = OMSerializer
+15
View File
@@ -0,0 +1,15 @@
from cola.prefs.view import PreferencesView
from cola.prefs.view import diff_font
from cola.prefs.view import tab_width
from cola.prefs.model import PreferencesModel
from cola.prefs.controller import PreferencesController
def preferences(model=None):
if model is None:
model = PreferencesModel()
prefs = PreferencesView(model)
ctl = PreferencesController(model, prefs)
prefs.show()
prefs.raise_()
return ctl
+8
View File
@@ -0,0 +1,8 @@
from cola.ctrl import Controller
from cola.prefs.model import command_directory
class PreferencesController(Controller):
def __init__(self, model, view):
Controller.__init__(self, model, view)
self.add_commands(command_directory)
+50
View File
@@ -0,0 +1,50 @@
from cola import gitcfg
from cola import observable
from cola.cmds import BaseCommand
class PreferencesModel(observable.Observable):
message_set_config = 'set_config'
message_config_updated = 'config_updated'
def __init__(self):
observable.Observable.__init__(self)
self.config = gitcfg.instance()
def set_config(self, source, config, value):
if source == 'repo':
self.config.set_repo(config, value)
else:
self.config.set_user(config, value)
message = self.message_config_updated
self.notify_message_observers(message, source, config, value)
def get_config(self, source, config):
if source == 'repo':
return self.config.get_repo(config)
else:
return self.config.get(config)
class SetConfigCommand(BaseCommand):
def __init__(self, source, config, value):
BaseCommand.__init__(self)
self.undoable = True
self.source = source
self.config = config
self.value = value
self.old_value = None
def do(self):
self.old_value = self.context.get_config(self.source, self.config)
self.context.set_config(self.source, self.config, self.value)
def undo(self):
if self.old_value is None:
return
self.context.set_config(self.source, self.config, self.old_value)
command_directory = {
PreferencesModel.message_set_config: SetConfigCommand,
}
+295
View File
@@ -0,0 +1,295 @@
import os
from PyQt4 import QtCore
from PyQt4 import QtGui
from PyQt4.QtCore import SIGNAL
from cola import qtutils
from cola import gitcfg
from cola.views import standard
from cola.qtutils import relay_signal
from cola.utils import is_darwin
class FormWidget(QtGui.QWidget):
def __init__(self, model, parent, source='user'):
QtGui.QWidget.__init__(self, parent)
self.model = model
self.config_to_widget = {}
self.widget_to_config = {}
self.source = source
self.config = gitcfg.instance()
self.defaults = {}
self.setLayout(QtGui.QFormLayout())
def add_row(self, label, widget):
self.layout().addRow(label, widget)
def set_config(self, config_dict):
self.config_to_widget.update(config_dict)
for config, (widget, default) in config_dict.items():
self.widget_to_config[config] = widget
self.defaults[config] = default
self.connect_widget_to_config(widget, config)
def connect_widget_to_config(self, widget, config):
if isinstance(widget, QtGui.QSpinBox):
widget.connect(widget, SIGNAL('valueChanged(int)'),
self._int_config_changed(config))
elif isinstance(widget, QtGui.QCheckBox):
widget.connect(widget, SIGNAL('toggled(bool)'),
self._bool_config_changed(config))
elif isinstance(widget, QtGui.QLineEdit):
widget.connect(widget, SIGNAL('editingFinished()'),
self._text_config_changed(config))
widget.connect(widget, SIGNAL('returnPressed()'),
self._text_config_changed(config))
def _int_config_changed(self, config):
def emitter(value):
self.emit(SIGNAL(self.model.message_set_config),
self.source, config, value)
return emitter
def _bool_config_changed(self, config):
def emitter(value):
self.emit(SIGNAL(self.model.message_set_config),
self.source, config, value)
return emitter
def _text_config_changed(self, config):
def emitter():
value = unicode(self.sender().text())
self.emit(SIGNAL(self.model.message_set_config),
self.source, config, value)
return emitter
def update_from_config(self):
if self.source == 'repo':
getter = self.config.get_repo
elif self.source == 'user':
getter = self.config.get_user
else:
getter = self.config.get
for config, widget in self.widget_to_config.items():
value = getter(config)
if value is None:
value = self.defaults[config]
self.set_widget_value(widget, value)
def set_widget_value(self, widget, value):
widget.blockSignals(True)
if isinstance(widget, QtGui.QSpinBox):
widget.setValue(value)
elif isinstance(widget, QtGui.QLineEdit):
widget.setText(value)
elif isinstance(widget, QtGui.QCheckBox):
widget.setChecked(value)
widget.blockSignals(False)
class RepoFormWidget(FormWidget):
def __init__(self, model, parent, source):
FormWidget.__init__(self, model, parent, source=source)
self.name = QtGui.QLineEdit()
self.email = QtGui.QLineEdit()
self.merge_verbosity = QtGui.QSpinBox()
self.merge_verbosity.setMinimum(0)
self.merge_verbosity.setMaximum(5)
self.merge_verbosity.setProperty('value', QtCore.QVariant(5))
self.diff_context = QtGui.QSpinBox()
self.diff_context.setMinimum(2)
self.diff_context.setMaximum(99)
self.diff_context.setProperty('value', QtCore.QVariant(5))
self.merge_summary = QtGui.QCheckBox()
self.merge_summary.setChecked(True)
self.merge_diffstat = QtGui.QCheckBox()
self.merge_diffstat.setChecked(True)
self.add_row('User Name', self.name)
self.add_row('Email Address', self.email)
self.add_row('Merge Verbosity', self.merge_verbosity)
self.add_row('Number of Diff Context Lines', self.diff_context)
self.add_row('Summarize Merge Commits', self.merge_summary)
self.add_row('Show Diffstat After Merge', self.merge_diffstat)
self.set_config({
'gui.diffcontext': (self.diff_context, 5),
'user.name': (self.name, ''),
'user.email': (self.email, ''),
'merge.diffstat': (self.merge_diffstat, True),
'merge.summary': (self.merge_summary, True),
'merge.verbosity': (self.merge_verbosity, 5),
})
class SettingsFormWidget(FormWidget):
def __init__(self, model, parent):
FormWidget.__init__(self, model, parent)
self.fixed_font = QtGui.QFontComboBox()
self.fixed_font.setFontFilters(QtGui.QFontComboBox.MonospacedFonts)
self.font_size = QtGui.QSpinBox()
self.font_size.setMinimum(8)
self.font_size.setProperty('value', QtCore.QVariant(12))
self._font_str = None
self.tab_width = QtGui.QSpinBox()
self.tab_width.setWrapping(True)
self.tab_width.setMaximum(42)
self.editor = QtGui.QLineEdit()
self.historybrowser = QtGui.QLineEdit()
self.difftool = QtGui.QLineEdit()
self.mergetool = QtGui.QLineEdit()
self.keep_merge_backups = QtGui.QCheckBox()
self.save_gui_settings = QtGui.QCheckBox()
self.add_row('Fixed-Width Font', self.fixed_font)
self.add_row('Font Size', self.font_size)
self.add_row('Tab Width', self.tab_width)
self.add_row('Editor', self.editor)
self.add_row('History Browser', self.historybrowser)
self.add_row('Diff Tool', self.difftool)
self.add_row('Merge Tool', self.mergetool)
self.add_row('Keep *.orig Merge Backups', self.keep_merge_backups)
self.add_row('Save GUI Settings', self.save_gui_settings)
self.set_config({
'cola.savewindowsettings': (self.save_gui_settings, True),
'cola.tabwidth': (self.tab_width, 8),
'diff.tool': (self.difftool, 'xxdiff'),
'gui.editor': (self.editor, os.getenv('VISUAL', 'gvim')),
'gui.historybrowser': (self.historybrowser, 'gitk'),
'merge.keepbackup': (self.keep_merge_backups, True),
'merge.tool': (self.mergetool, 'xxdiff'),
})
self.connect(self.fixed_font, SIGNAL('currentFontChanged(const QFont &)'),
self.current_font_changed)
self.connect(self.font_size, SIGNAL('valueChanged(int)'),
self.font_size_changed)
def update_from_config(self):
FormWidget.update_from_config(self)
self.fixed_font.blockSignals(True)
self.font_size.blockSignals(True)
font = diff_font()
font_size = font.pointSize()
self.fixed_font.setCurrentFont(font)
self.font_size.setValue(font_size)
self.fixed_font.blockSignals(False)
self.font_size.blockSignals(False)
def font_size_changed(self, size):
font = self.fixed_font.currentFont()
font.setPointSize(size)
self.emit(SIGNAL(self.model.message_set_config),
'user', 'cola.fontdiff', unicode(font.toString()))
def current_font_changed(self, font):
self.emit(SIGNAL(self.model.message_set_config),
'user', 'cola.fontdiff', unicode(font.toString()))
class PreferencesView(standard.Dialog):
def __init__(self, model, parent=None):
standard.Dialog.__init__(self, parent=parent)
self.setWindowTitle(self.tr('Preferences'))
self.resize(600, 360)
self._tabbar = QtGui.QTabBar()
self._tabbar.setDrawBase(False)
self._tabbar.addTab('All Repositories')
self._tabbar.addTab('Current Repository')
self._tabbar.addTab('Settings')
self._user_form = RepoFormWidget(model, self, source='user')
self._repo_form = RepoFormWidget(model, self, source='all')
self._options_form = SettingsFormWidget(model, self)
relay_signal(self, self._user_form, SIGNAL(model.message_set_config))
relay_signal(self, self._repo_form, SIGNAL(model.message_set_config))
relay_signal(self, self._options_form, SIGNAL(model.message_set_config))
self._stackedwidget = QtGui.QStackedWidget()
self._stackedwidget.addWidget(self._user_form)
self._stackedwidget.addWidget(self._repo_form)
self._stackedwidget.addWidget(self._options_form)
self.close_button = QtGui.QPushButton(self)
self.close_button.setText(qtutils.tr('Close'))
self.close_button.setIcon(qtutils.close_icon())
self._button_layt = QtGui.QHBoxLayout()
self._button_layt.setMargin(0)
self._button_layt.addStretch()
self._button_layt.addWidget(self.close_button)
self._layt = QtGui.QVBoxLayout()
self._layt.setMargin(4)
self._layt.addWidget(self._tabbar)
self._layt.addWidget(self._stackedwidget)
self._layt.addLayout(self._button_layt)
self.setLayout(self._layt)
self.connect(self._tabbar, SIGNAL('currentChanged(int)'),
self._stackedwidget.setCurrentIndex)
self.connect(self._stackedwidget, SIGNAL('currentChanged(int)'),
self.update_widget)
self.connect(self.close_button, SIGNAL('clicked()'), self.accept)
qtutils.add_close_action(self)
self.update_widget(0)
def update_widget(self, idx):
widget = self._stackedwidget.widget(idx)
widget.update_from_config()
def tab_width():
return gitcfg.instance().get('cola.tabwidth', 8)
def diff_font_str():
font_str = gitcfg.instance().get('cola.fontdiff')
if font_str is None:
font = QtGui.QFont()
family = 'Monospace'
if is_darwin():
family = 'Monaco'
font.setFamily(family)
font_str = unicode(font.toString())
return font_str
def diff_font():
font_str = diff_font_str()
font = QtGui.QFont()
font.fromString(font_str)
return font
if __name__ == "__main__":
import sys
from cola.prefs import preferences
app = QtGui.QApplication(sys.argv)
preferences()
sys.exit(app.exec_())
+14 -34
View File
@@ -10,12 +10,12 @@ are always available in the model without having to worry about the
different ways to query Qt widgets.
"""
import types
from PyQt4 import QtCore
from PyQt4 import QtGui
from PyQt4 import QtCore
from PyQt4.QtCore import SIGNAL
from cola import observer
from cola.compat import set
class QObserver(observer.Observer, QtCore.QObject):
@@ -32,28 +32,22 @@ class QObserver(observer.Observer, QtCore.QObject):
self._model_to_view = {}
self._mapped_params = set()
self._connected = set()
self._in_textfield = False
self._in_callback = False
def SLOT(self, *args):
"""Default slot to handle all Qt callbacks.
This method delegates to callbacks from add_signals."""
self._in_textfield = False
widget = self.sender()
param = self._widget_names[widget]
if param in self._mapped_params:
model = self.model
if isinstance(widget, QtGui.QTextEdit):
self._in_textfield = True
value = unicode(widget.toPlainText())
model.set_param(param, value, notify=False)
elif isinstance(widget, QtGui.QLineEdit):
self._in_textfield = True
value = unicode(widget.text())
model.set_param(param, value)
model.set_param(param, value, notify=False)
elif isinstance(widget, QtGui.QCheckBox):
model.set_param(param, widget.isChecked())
elif isinstance(widget, QtGui.QSpinBox):
@@ -107,24 +101,13 @@ class QObserver(observer.Observer, QtCore.QObject):
else:
print("SLOT(): Unknown widget:", param, widget)
self._in_callback = True
if param in self._callbacks:
self._callbacks[param](*args)
self._in_callback = False
self._in_textfield = False
def connect(self, obj, signal_str, *args):
"""Convenience function so that subclasses do not have
to import QtCore.SIGNAL."""
signal = signal_str
if type(signal) is str:
signal = QtCore.SIGNAL(signal)
return QtCore.QObject.connect(obj, signal, *args)
def add_signals(self, signal_str, *objects):
"""Connects object's signal to the QObserver."""
for obj in objects:
self.connect(obj, signal_str, self.SLOT)
self.connect(obj, SIGNAL(signal_str), self.SLOT)
def add_callbacks(self, **callbacks):
"""Registers callbacks that are called in response to GUI events."""
@@ -160,18 +143,20 @@ class QObserver(observer.Observer, QtCore.QObject):
self.add_signals('itemClicked(QListWidgetItem *)', widget)
doubleclick = str(widget.objectName())+'_doubleclick'
if hasattr(self, doubleclick):
self.connect(widget, 'itemDoubleClicked(QListWidgetItem *)',
self.connect(widget,
SIGNAL('itemDoubleClicked(QListWidgetItem *)'),
getattr(self, doubleclick))
elif isinstance(widget, QtGui.QTreeWidget):
self.add_signals('itemSelectionChanged()', widget)
self.add_signals('itemClicked(QTreeWidgetItem *, int)', widget)
doubleclick = str(widget.objectName())+'_doubleclick'
if hasattr(self, doubleclick):
self.connect(widget, 'itemDoubleClicked(QTreeWidgetItem *, int)',
self.connect(widget,
SIGNAL('itemDoubleClicked(QTreeWidgetItem *, int)'),
getattr(self, doubleclick))
elif isinstance(widget, QtGui.QAbstractButton):
self.add_signals('released()', widget)
self.add_signals('clicked()', widget)
elif isinstance(widget, QtGui.QAction):
self.add_signals('triggered()', widget)
elif isinstance(widget, QtGui.QCheckBox):
@@ -194,9 +179,9 @@ class QObserver(observer.Observer, QtCore.QObject):
def add_actions(self, **kwargs):
"""
Register view actions.
Action are called in response to view changes.(view->model).
"""
for param, callback in kwargs.iteritems():
if type(callback) is list:
@@ -207,12 +192,6 @@ class QObserver(observer.Observer, QtCore.QObject):
def subject_changed(self, param, value):
"""Sends a model param to the view(model->view)"""
if self._in_textfield and not self._in_callback:
# A slot has changed the model and we're not in
# a user callback. In this case the event is causing
# a feedback loop so skip redundant work and return.
return
if param in self._model_to_view:
# Temporarily disable notification to avoid loop-backs
notify = self.model.notification_enabled
@@ -224,7 +203,7 @@ class QObserver(observer.Observer, QtCore.QObject):
elif isinstance(widget, QtGui.QPixmap):
widget.load(value)
elif isinstance(widget, QtGui.QTextEdit):
widget.setText(value)
widget.setPlainText(value)
elif isinstance(widget, QtGui.QLineEdit):
widget.setText(value)
elif isinstance(widget, QtGui.QListWidget):
@@ -295,6 +274,7 @@ class QObserver(observer.Observer, QtCore.QObject):
else:
print('subject_changed(): Unknown widget:',
str(widget.objectName()), widget, value)
self.model.notification_enabled = notify
if param not in self._actions:
+688
View File
@@ -0,0 +1,688 @@
import re
import subprocess
from PyQt4 import QtGui
from PyQt4 import QtCore
from PyQt4.QtCore import Qt
from PyQt4.QtCore import QVariant
from PyQt4.QtCore import SIGNAL
from PyQt4.QtGui import QFont
from PyQt4.QtGui import QSyntaxHighlighter
from PyQt4.QtGui import QTextCharFormat
from PyQt4.QtGui import QColor
from PyQt4.QtCore import pyqtProperty
import cola
from cola import resources
from cola import utils
from cola import qtutils
from cola.compat import set
from cola.qtutils import tr
def create_button(text, layout=None, tooltip=None, icon=None):
"""Create a button, set its title, and add it to the parent."""
button = QtGui.QPushButton()
button.setText(tr(text))
if icon:
button.setIcon(icon)
if layout is not None:
layout.addWidget(button)
return button
def create_dock(title, parent):
"""Create a dock widget and set it up accordingly."""
dock = QtGui.QDockWidget(parent)
dock.setWindowTitle(tr(title))
dock.setObjectName(title)
return dock
def create_menu(title, parent):
"""Create a menu and set its title."""
qmenu = QtGui.QMenu(parent)
qmenu.setTitle(tr(title))
return qmenu
def create_toolbutton(parent, text=None, layout=None, tooltip=None, icon=None):
button = QtGui.QToolButton(parent)
button.setAutoRaise(True)
button.setAutoFillBackground(True)
if icon:
button.setIcon(icon)
if text:
button.setText(tr(text))
button.setToolButtonStyle(Qt.ToolButtonTextBesideIcon)
if tooltip:
button.setToolTip(tr(tooltip))
if layout is not None:
layout.addWidget(button)
return button
class QFlowLayoutWidget(QtGui.QWidget):
_horizontal = QtGui.QBoxLayout.LeftToRight
_vertical = QtGui.QBoxLayout.TopToBottom
def __init__(self, parent=None):
QtGui.QWidget.__init__(self, parent)
self._direction = self._vertical
self._layout = layout = QtGui.QBoxLayout(self._direction)
layout.setSpacing(2)
layout.setMargin(2)
self.setLayout(layout)
self.setContentsMargins(2, 2, 2, 2)
policy = QtGui.QSizePolicy(QtGui.QSizePolicy.Minimum,
QtGui.QSizePolicy.Minimum)
self.setSizePolicy(policy)
self.setMinimumSize(QtCore.QSize(1, 1))
def resizeEvent(self, event):
size = event.size()
if size.width() * 0.8 < size.height():
dxn = self._vertical
else:
dxn = self._horizontal
if dxn != self._direction:
self._direction = dxn
self.layout().setDirection(dxn)
class QCollapsibleGroupBox(QtGui.QGroupBox):
def __init__(self, parent=None):
QtGui.QGroupBox.__init__(self, parent)
self.setFlat(True)
self.collapsed = False
self.click_pos = None
self.collapse_icon_size = 16
def set_collapsed(self, collapsed):
self.collapsed = collapsed
for widget in self.findChildren(QtGui.QWidget):
widget.setHidden(collapsed)
self.emit(SIGNAL('toggled(bool)'), collapsed)
def mousePressEvent(self, event):
if event.button() == QtCore.Qt.LeftButton:
option = QtGui.QStyleOptionGroupBox()
self.initStyleOption(option)
icon_size = self.collapse_icon_size
button_area = QtCore.QRect(0, 0, icon_size, icon_size)
top_left = option.rect.adjusted(0, 0, -10, 0).topLeft()
button_area.moveTopLeft(QtCore.QPoint(top_left))
self.click_pos = event.pos()
QtGui.QGroupBox.mousePressEvent(self, event)
def mouseReleaseEvent(self, event):
if (event.button() == QtCore.Qt.LeftButton and
self.click_pos == event.pos()):
self.set_collapsed(not self.collapsed)
QtGui.QGroupBox.mouseReleaseEvent(self, event)
def paintEvent(self, event):
painter = QtGui.QStylePainter(self)
option = QtGui.QStyleOptionGroupBox()
self.initStyleOption(option)
painter.save()
painter.translate(self.collapse_icon_size + 4, 0)
painter.drawComplexControl(QtGui.QStyle.CC_GroupBox, option)
painter.restore()
style = QtGui.QStyle
point = option.rect.adjusted(0, 0, -10, 0).topLeft()
icon_size = self.collapse_icon_size
option.rect = QtCore.QRect(point.x(), point.y(), icon_size, icon_size)
if self.collapsed:
painter.drawPrimitive(style.PE_IndicatorArrowRight, option)
else:
painter.drawPrimitive(style.PE_IndicatorArrowDown, option)
class GitRefCompleter(QtGui.QCompleter):
"""Provides completion for branches and tags"""
def __init__(self, parent):
QtGui.QCompleter.__init__(self, parent)
self._model = GitRefModel(parent)
self.setModel(self._model)
self.setCompletionMode(self.UnfilteredPopupCompletion)
self.setCaseSensitivity(QtCore.Qt.CaseInsensitive)
def __del__(self):
self.dispose()
def dispose(self):
self._model.dispose()
class GitRefLineEdit(QtGui.QLineEdit):
def __init__(self, parent=None):
QtGui.QLineEdit.__init__(self, parent)
self.refcompleter = GitRefCompleter(self)
self.setCompleter(self.refcompleter)
class GitRefModel(QtGui.QStandardItemModel):
def __init__(self, parent):
QtGui.QStandardItemModel.__init__(self, parent)
self.cmodel = cola.model()
msg = self.cmodel.message_updated
self.cmodel.add_message_observer(msg, self.update_matches)
self.update_matches()
def dispose(self):
self.cmodel.remove_observer(self.update_matches)
def update_matches(self):
model = self.cmodel
matches = model.local_branches + model.remote_branches + model.tags
QStandardItem = QtGui.QStandardItem
self.clear()
for match in matches:
item = QStandardItem()
item.setIcon(QtGui.QIcon(resources.icon('git.svg')))
item.setText(match)
self.appendRow(item)
class GitLogCompletionModel(QtGui.QStandardItemModel):
def __init__(self, parent):
self.matched_text = None
QtGui.QStandardItemModel.__init__(self, parent)
self.cmodel = cola.model()
def lower_cmp(self, a, b):
return cmp(a.replace('.','').lower(), b.replace('.','').lower())
def update_matches(self, case_sensitive):
QStandardItem = QtGui.QStandardItem
file_list = self.cmodel.everything()
files = set(file_list)
files_and_dirs = utils.add_parents(set(files))
dirs = files_and_dirs.difference(files)
file_icon = qtutils.file_icon()
dir_icon = qtutils.dir_icon()
git_icon = QtGui.QIcon(resources.icon('git.svg'))
model = self.cmodel
refs = model.local_branches + model.remote_branches + model.tags
matched_text = self.matched_text
if matched_text:
if case_sensitive:
matched_refs = [r for r in refs if matched_text in r]
else:
matched_refs = [r for r in refs
if matched_text.lower() in r.lower()]
else:
matched_refs = refs
matched_refs.sort(cmp=self.lower_cmp)
if matched_text:
if case_sensitive:
matched_paths = [f for f in files_and_dirs
if matched_text in f]
else:
matched_paths = [f for f in files_and_dirs
if matched_text.lower() in f.lower()]
else:
matched_paths = list(files_and_dirs)
matched_paths.sort(cmp=self.lower_cmp)
items = []
for ref in matched_refs:
item = QStandardItem()
item.setText(ref)
item.setIcon(git_icon)
items.append(item)
if matched_paths and (not matched_text or matched_text in '--'):
item = QStandardItem()
item.setText('--')
item.setIcon(file_icon)
items.append(item)
for match in matched_paths:
item = QStandardItem()
item.setText(match)
if match in dirs:
item.setIcon(dir_icon)
else:
item.setIcon(file_icon)
items.append(item)
self.clear()
for item in items:
self.appendRow(item)
def set_match_text(self, text, case_sensitive):
self.matched_text = text
self.update_matches(case_sensitive)
class GitLogLineEdit(QtGui.QLineEdit):
def __init__(self, parent=None):
QtGui.QLineEdit.__init__(self, parent)
# used to hide the completion popup after a drag-select
self._drag = 0
self._model = GitLogCompletionModel(self)
self._delegate = HighlightCompletionDelegate(self)
self._completer = QtGui.QCompleter(self)
self._completer.setWidget(self)
self._completer.setModel(self._model)
self._completer.setCompletionMode(
QtGui.QCompleter.UnfilteredPopupCompletion)
self._completer.popup().setItemDelegate(self._delegate)
self.connect(self._completer, SIGNAL('activated(QString)'),
self._complete)
self.connect(self, SIGNAL('textChanged(QString)'), self._text_changed)
self._keys_to_ignore = set([QtCore.Qt.Key_Enter,
QtCore.Qt.Key_Return,
QtCore.Qt.Key_Escape])
def is_case_sensitive(self, text):
return bool([char for char in text if char.isupper()])
def _text_changed(self, text):
text = self.last_word()
case_sensitive = self.is_case_sensitive(text)
if case_sensitive:
self._completer.setCaseSensitivity(QtCore.Qt.CaseSensitive)
else:
self._completer.setCaseSensitivity(QtCore.Qt.CaseInsensitive)
self._delegate.set_highlight_text(text, case_sensitive)
self._model.set_match_text(text, case_sensitive)
def update_matches(self):
text = self.last_word()
case_sensitive = self.is_case_sensitive(text)
self._model.update_matches(case_sensitive)
def _complete(self, completion):
"""
This is the event handler for the QCompleter.activated(QString) signal,
it is called when the user selects an item in the completer popup.
"""
if not completion:
return
words = self.words()
if words:
words.pop()
words.append(unicode(completion))
self.setText(subprocess.list2cmdline(words))
self.emit(SIGNAL('ref_changed'))
def words(self):
return utils.shell_usplit(unicode(self.text()))
def last_word(self):
words = self.words()
if not words:
return unicode(self.text())
if not words[-1]:
return u''
return words[-1]
def event(self, event):
if event.type() == QtCore.QEvent.KeyPress:
if (event.key() == QtCore.Qt.Key_Tab and
self._completer.popup().isVisible()):
event.ignore()
return True
return QtGui.QLineEdit.event(self, event)
def do_completion(self):
self._completer.popup().setCurrentIndex(
self._completer.completionModel().index(0,0))
self._completer.complete()
def keyPressEvent(self, event):
if self._completer.popup().isVisible():
if event.key() in self._keys_to_ignore:
event.ignore()
self._complete(self.last_word())
return
elif (event.key() == QtCore.Qt.Key_Down and
self._completer.completionCount() > 0):
event.accept()
self.do_completion()
return
QtGui.QLineEdit.keyPressEvent(self, event)
prefix = self.last_word()
if prefix != unicode(self._completer.completionPrefix()):
self._update_popup_items(prefix)
if len(event.text()) > 0 and len(prefix) > 0:
self._completer.complete()
if len(prefix) == 0:
self._completer.popup().hide()
#: _drag: 0 - unclicked, 1 - clicked, 2 - dragged
def mousePressEvent(self, event):
self._drag = 1
return QtGui.QLineEdit.mousePressEvent(self, event)
def mouseMoveEvent(self, event):
if self._drag == 1:
self._drag = 2
return QtGui.QLineEdit.mouseMoveEvent(self, event)
def mouseReleaseEvent(self, event):
if self._drag != 2 and event.buttons() != QtCore.Qt.RightButton:
self.do_completion()
self._drag = 0
return QtGui.QLineEdit.mouseReleaseEvent(self, event)
def close_popup(self):
self._completer.popup().close()
def _update_popup_items(self, prefix):
"""
Filters the completer's popup items to only show items
with the given prefix.
"""
self._completer.setCompletionPrefix(prefix)
self._completer.popup().setCurrentIndex(
self._completer.completionModel().index(0,0))
class HighlightCompletionDelegate(QtGui.QStyledItemDelegate):
"""A delegate used for auto-completion to give formatted completion"""
def __init__(self, parent=None): # model, parent=None):
QtGui.QStyledItemDelegate.__init__(self, parent)
self.highlight_text = ''
self.case_sensitive = False
self.doc = QtGui.QTextDocument()
self.doc.setDocumentMargin(0)
def set_highlight_text(self, text, case_sensitive):
"""Sets the text that will be made bold in the term name when displayed"""
self.highlight_text = text
self.case_sensitive = case_sensitive
def paint(self, painter, option, index):
"""Overloaded Qt method for custom painting of a model index"""
if not self.highlight_text:
return QtGui.QStyledItemDelegate.paint(self, painter, option, index)
text = unicode(index.data().toPyObject())
if self.case_sensitive:
html = text.replace(self.highlight_text,
'<strong>%s</strong>' % self.highlight_text)
else:
match = re.match('(.*)(' + self.highlight_text + ')(.*)',
text, re.IGNORECASE)
if match:
start = match.group(1) or ''
middle = match.group(2) or ''
end = match.group(3) or ''
html = (start + ('<strong>%s</strong>' % middle) + end)
else:
html = text
self.doc.setHtml(html)
# Painting item without text, Text Document will paint the text
optionV4 = QtGui.QStyleOptionViewItemV4(option)
self.initStyleOption(optionV4, index)
optionV4.text = QtCore.QString()
style = QtGui.QApplication.style()
style.drawControl(QtGui.QStyle.CE_ItemViewItem, optionV4, painter)
ctx = QtGui.QAbstractTextDocumentLayout.PaintContext()
# Highlighting text if item is selected
if (optionV4.state & QtGui.QStyle.State_Selected):
ctx.palette.setColor(QtGui.QPalette.Text, optionV4.palette.color(QtGui.QPalette.Active, QtGui.QPalette.HighlightedText))
# translate the painter to where the text is drawn
textRect = style.subElementRect(QtGui.QStyle.SE_ItemViewItemText, optionV4)
painter.save()
start = textRect.topLeft() + QtCore.QPoint(3, 0)
painter.translate(start)
painter.setClipRect(textRect.translated(-start))
# tell the text document to draw the html for us
self.doc.documentLayout().draw(painter, ctx)
painter.restore()
# Syntax highlighting
def TERMINAL(pattern):
"""
Denotes that a pattern is the final pattern that should
be matched. If this pattern matches no other formats
will be applied, even if they would have matched.
"""
return '__TERMINAL__:%s' % pattern
# Cache the results of re.compile so that we don't keep
# rebuilding the same regexes whenever stylesheets change
_RGX_CACHE = {}
default_colors = {}
def _install_default_colors():
def color(c, a=255):
qc = QColor(c)
qc.setAlpha(a)
return qc
default_colors.update({
'color_add': color(Qt.green, 128),
'color_remove': color(Qt.red, 128),
'color_begin': color(Qt.darkCyan),
'color_header': color(Qt.darkYellow),
'color_stat_add': color(QColor(32, 255, 32)),
'color_stat_info': color(QColor(32, 32, 255)),
'color_stat_remove': color(QColor(255, 32, 32)),
'color_emphasis': color(Qt.black),
'color_info': color(Qt.blue),
'color_date': color(Qt.darkCyan),
})
_install_default_colors()
class GenericSyntaxHighligher(QSyntaxHighlighter):
def __init__(self, doc, *args, **kwargs):
QSyntaxHighlighter.__init__(self, doc)
for attr, val in default_colors.items():
setattr(self, attr, val)
self._rules = []
self.generate_rules()
self.reset()
def reset(self):
self._rules = []
self.generate_rules()
def generate_rules(self):
pass
def create_rules(self, *rules):
if len(rules) % 2:
raise Exception('create_rules requires an even '
'number of arguments.')
for idx, rule in enumerate(rules):
if idx % 2:
continue
formats = rules[idx+1]
terminal = rule.startswith(TERMINAL(''))
if terminal:
rule = rule[len(TERMINAL('')):]
try:
regex = _RGX_CACHE[rule]
except KeyError:
regex = _RGX_CACHE[rule] = re.compile(rule)
self._rules.append((regex, formats, terminal,))
def formats(self, line):
matched = []
for regex, fmts, terminal in self._rules:
match = regex.match(line)
if not match:
continue
matched.append([match, fmts])
if terminal:
return matched
return matched
def mkformat(self, fg=None, bg=None, bold=False):
fmt = QTextCharFormat()
if fg:
fmt.setForeground(fg)
if bg:
fmt.setBackground(bg)
if bold:
fmt.setFontWeight(QFont.Bold)
return fmt
def highlightBlock(self, qstr):
ascii = unicode(qstr)
if not ascii:
return
formats = self.formats(ascii)
if not formats:
return
for match, fmts in formats:
start = match.start()
end = match.end()
groups = match.groups()
# No groups in the regex, assume this is a single rule
# that spans the entire line
if not groups:
self.setFormat(0, len(ascii), fmts)
continue
# Groups exist, rule is a tuple corresponding to group
for grpidx, group in enumerate(groups):
# allow empty matches
if not group:
continue
# allow None as a no-op format
length = len(group)
if fmts[grpidx]:
self.setFormat(start, start+length,
fmts[grpidx])
start += length
def set_colors(self, colordict):
for attr, val in colordict.items():
setattr(self, attr, val)
class DiffSyntaxHighlighter(GenericSyntaxHighligher):
"""Implements the diff syntax highlighting
This class is used by widgets that display diffs.
"""
def __init__(self, doc, whitespace=True):
self.whitespace = whitespace
GenericSyntaxHighligher.__init__(self, doc)
def generate_rules(self):
diff_begin = self.mkformat(self.color_begin, bold=True)
diff_head = self.mkformat(self.color_header)
diff_add = self.mkformat(bg=self.color_add)
diff_remove = self.mkformat(bg=self.color_remove)
diffstat_info = self.mkformat(self.color_stat_info, bold=True)
diffstat_add = self.mkformat(self.color_stat_add, bold=True)
diffstat_remove = self.mkformat(self.color_stat_remove, bold=True)
if self.whitespace:
bad_ws = self.mkformat(Qt.black, Qt.red)
# We specify the whitespace rule last so that it is
# applied after the diff addition/removal rules.
# The rules for the header
diff_bgn_rgx = TERMINAL('^@@|^\+\+\+|^---')
diff_hd1_rgx = TERMINAL('^diff --git')
diff_hd2_rgx = TERMINAL('^index \S+\.\.\S+')
diff_hd3_rgx = TERMINAL('^new file mode')
diff_add_rgx = TERMINAL('^\+')
diff_rmv_rgx = TERMINAL('^-')
diff_sts_rgx = ('(.+\|.+?)(\d+)(.+?)([\+]*?)([-]*?)$')
diff_sum_rgx = ('(\s+\d+ files changed[^\d]*)'
'(:?\d+ insertions[^\d]*)'
'(:?\d+ deletions.*)$')
self.create_rules(diff_bgn_rgx, diff_begin,
diff_hd1_rgx, diff_head,
diff_hd2_rgx, diff_head,
diff_hd3_rgx, diff_head,
diff_add_rgx, diff_add,
diff_rmv_rgx, diff_remove,
diff_sts_rgx, (None, diffstat_info,
None, diffstat_add,
diffstat_remove),
diff_sum_rgx, (diffstat_info,
diffstat_add,
diffstat_remove))
if self.whitespace:
self.create_rules('(..*?)(\s+)$', (None, bad_ws))
# This is used as a mixin to generate property callbacks
def accessors(attr):
private_attr = '_'+attr
def getter(self):
return self.__dict__.get(private_attr, None)
def setter(self, value):
self.__dict__[private_attr] = value
self.reset_syntax()
return (getter, setter)
def install_style_properties(cls):
# Diff GUI colors -- this is controllable via the style sheet
if pyqtProperty is None:
return
for name in default_colors:
setattr(cls, name, pyqtProperty('QColor', *accessors(name)))
def set_theme_properties(widget):
for name, color in default_colors.items():
widget.setProperty(name, QVariant(color))
if __name__ == '__main__':
import sys
class SyntaxTestDialog(QtGui.QDialog):
def __init__(self, parent):
QtGui.QDialog.__init__(self, parent)
self.resize(720, 512)
self.vboxlayout = QtGui.QVBoxLayout(self)
self.vboxlayout.setObjectName('vboxlayout')
self.output_text = QtGui.QTextEdit(self)
font = QtGui.QFont()
if utils.is_darwin():
family = 'Monaco'
else:
family = 'Monospace'
font.setFamily(family)
font.setPointSize(13)
self.output_text.setFont(font)
self.output_text.setAcceptDrops(False)
self.vboxlayout.addWidget(self.output_text)
self.syntax = DiffSyntaxHighlighter(self.output_text.document())
app = QtGui.QApplication(sys.argv)
dialog = SyntaxTestDialog(app.activeWindow())
dialog.show()
dialog.raise_()
app.exec_()
+25
View File
@@ -0,0 +1,25 @@
from PyQt4 import QtGui
from PyQt4 import QtCore
def install():
if not hasattr(QtGui.QHBoxLayout, 'setContentsMargins'):
QtGui.QHBoxLayout.setContentsMargins = lambda *args: True
if not hasattr(QtGui.QVBoxLayout, 'setContentsMargins'):
QtGui.QVBoxLayout.setContentsMargins = lambda *args: True
if not hasattr(QtGui.QKeySequence, 'Preferences'):
QtGui.QKeySequence.Preferences = 'Ctrl+O'
def add_search_path(prefix, path):
if hasattr(QtCore.QDir, 'addSearchPath'):
QtCore.QDir.addSearchPath(prefix, path)
def set_common_dock_options(window):
if not hasattr(window, 'setDockOptions'):
return
nested = QtGui.QMainWindow.AllowNestedDocks
tabbed = QtGui.QMainWindow.AllowTabbedDocks
animated = QtGui.QMainWindow.AnimatedDocks
window.setDockOptions(nested | tabbed | animated)
+240 -55
View File
@@ -3,23 +3,28 @@
"""
import os
from PyQt4 import QtCore
from PyQt4 import QtGui
from PyQt4 import QtCore
from PyQt4.QtCore import Qt
from PyQt4.QtCore import SIGNAL
import cola
from cola import core
from cola import gitcfg
from cola import utils
from cola import settings
from cola import signals
from cola import resources
import cola.views.log
from cola.compat import set
from cola.decorators import memoize
from cola.widgets.log import LogView
_logger = None
@memoize
def logger():
global _logger
if not _logger:
_logger = cola.views.log.LogView()
cola.notifier().listen(signals.log_cmd, _logger.log)
return _logger
logview = LogView()
cola.notifier().connect(signals.log_cmd, logview.log)
return logview
def log(status, output):
@@ -29,10 +34,21 @@ def log(status, output):
return
cola.notifier().broadcast(signals.log_cmd, status, output)
def emit(widget, signal, *args, **opts):
"""Return a function that emits a signal"""
def emitter(*local_args, **local_opts):
if args or opts:
widget.emit(SIGNAL(signal), *args, **opts)
else:
widget.emit(SIGNAL(signal), *local_args, **local_opts)
return emitter
def SLOT(signal, *args, **opts):
"""
Returns a callback that broadcasts a message over the notifier.
If the caller of SLOT() provides args or opts then those are
used instead of the ones provided by the invoker of the callback.
@@ -45,6 +61,22 @@ def SLOT(signal, *args, **opts):
return broadcast
def connect_button(button, callback):
button.connect(button, SIGNAL('clicked()'), callback)
def relay_button(button, signal):
connect_button(button, SLOT(signal))
def relay_signal(parent, child, signal):
"""Relay a signal from the child widget through the parent"""
def relay_slot(*args, **opts):
parent.emit(signal, *args, **opts)
parent.connect(child, signal, relay_slot)
return relay_slot
def prompt(msg, title=None):
"""Presents the user with an input widget and returns the input."""
if title is None:
@@ -55,6 +87,7 @@ def prompt(msg, title=None):
result = QtGui.QInputDialog.getText(parent, msg, title)
return (unicode(result[0]), result[1])
def create_listwidget_item(text, filename):
"""Creates a QListWidgetItem with text and the icon at filename."""
item = QtGui.QListWidgetItem()
@@ -62,28 +95,69 @@ def create_listwidget_item(text, filename):
item.setText(text)
return item
_tree_icon_cache = {}
def create_treewidget_item(text, filename):
"""Creates a QTreeWidgetItem with text and the icon at filename."""
if filename not in _tree_icon_cache:
_tree_icon_cache[filename] = QtGui.QIcon(filename)
icon = _tree_icon_cache[filename]
icon = cached_icon_from_path(filename)
item = QtGui.QTreeWidgetItem()
item.setIcon(0, icon)
item.setText(0, text)
return item
def information(title, message=None):
"""Launches a QMessageBox information with the
provided title and message."""
@memoize
def cached_icon_from_path(filename):
return QtGui.QIcon(filename)
def information(title, message=None, parent=None, details=None, informative_text=None):
"""Show information with the provided title and message."""
if message is None:
message = title
title = tr(title)
message = tr(message)
if parent is None:
parent = QtGui.QApplication.instance().activeWindow()
mbox = QtGui.QMessageBox(parent)
mbox.setStandardButtons(QtGui.QMessageBox.Close)
mbox.setDefaultButton(QtGui.QMessageBox.Close)
mbox.setWindowTitle(title)
mbox.setWindowModality(QtCore.Qt.WindowModal)
mbox.setTextFormat(QtCore.Qt.PlainText)
mbox.setText(message)
if informative_text:
mbox.setInformativeText(tr(informative_text))
if details:
mbox.setDetailedText(details)
# Render git.svg into a 1-inch wide pixmap
pixmap = QtGui.QPixmap(resources.icon('git.svg'))
xres = pixmap.physicalDpiX()
pixmap = pixmap.scaledToHeight(xres, QtCore.Qt.SmoothTransformation)
mbox.setIconPixmap(pixmap)
mbox.exec_()
def critical(title, message=None, details=None):
"""Show a warning with the provided title and message."""
if message is None:
message = title
title = tr(title)
message = tr(message)
parent = QtGui.QApplication.instance().activeWindow()
QtGui.QMessageBox.information(parent, title, message)
mbox = QtGui.QMessageBox(parent)
mbox.setWindowTitle(title)
mbox.setTextFormat(QtCore.Qt.PlainText)
mbox.setText(message)
mbox.setIcon(QtGui.QMessageBox.Critical)
mbox.setStandardButtons(QtGui.QMessageBox.Close)
mbox.setDefaultButton(QtGui.QMessageBox.Close)
if details:
mbox.setDetailedText(details)
mbox.exec_()
# Register globally with the notifier
cola.notifier().listen(signals.information, information)
cola.notifier().connect(signals.information, information)
def selected_treeitem(tree_widget):
"""Returns a(id_number, is_selected) for a QTreeWidget."""
@@ -95,12 +169,15 @@ def selected_treeitem(tree_widget):
selected = True
return(id_number, selected)
def selected_row(list_widget):
"""Returns a(row_number, is_selected) tuple for a QListWidget."""
row = list_widget.currentRow()
item = list_widget.item(row)
selected = item is not None and item.isSelected()
return(row, selected)
items = list_widget.selectedItems()
if not items:
return (-1, False)
item = items[0]
return (list_widget.row(item), True)
def selection_list(listwidget, items):
"""Returns an array of model items that correspond to
@@ -114,6 +191,7 @@ def selection_list(listwidget, items):
selected.append(item)
return selected
def tree_selection(treeitem, items):
"""Returns model items that correspond to selected widget indices"""
itemcount = treeitem.childCount()
@@ -125,20 +203,27 @@ def tree_selection(treeitem, items):
return selected
def selected_item(list_widget, items):
"""Returns the selected item in a QListWidget."""
row, selected = selected_row(list_widget)
if selected and row < len(items):
widget_items = list_widget.selectedItems()
if not widget_items:
return None
widget_item = widget_items[0]
row = list_widget.row(widget_item)
if row < len(items):
return items[row]
else:
return None
def open_dialog(parent, title, filename=None):
"""Creates an Open File dialog and returns a filename."""
title_tr = tr(title)
return unicode(QtGui.QFileDialog
.getOpenFileName(parent, title_tr, filename))
def opendir_dialog(parent, title, path):
"""Prompts for a directory path"""
@@ -156,10 +241,12 @@ def save_dialog(parent, title, filename=''):
return unicode(QtGui.QFileDialog
.getSaveFileName(parent, title_tr, filename))
def icon(basename):
"""Given a basename returns a QIcon from the corresponding cola icon."""
return QtGui.QIcon(resources.icon(basename))
def question(parent, title, message, default=True):
"""Launches a QMessageBox question with the provided title and message.
Passing "default=False" will make "No" the default choice."""
@@ -176,6 +263,22 @@ def question(parent, title, message, default=True):
buttons, default)
return result == QtGui.QMessageBox.Yes
def confirm(parent, title, text, informative_text, ok_text, icon=None):
"""Confirm that an action should take place"""
if icon is None:
icon = ok_icon()
msgbox = QtGui.QMessageBox(parent)
msgbox.setWindowTitle(tr(title))
msgbox.setText(tr(text))
msgbox.setInformativeText(tr(informative_text))
ok = msgbox.addButton(tr(ok_text), QtGui.QMessageBox.ActionRole)
ok.setIcon(icon)
msgbox.addButton(QtGui.QMessageBox.Cancel)
msgbox.exec_()
return msgbox.clickedButton() == ok
def set_clipboard(text):
"""Sets the copy/paste buffer to text."""
if not text:
@@ -184,6 +287,18 @@ def set_clipboard(text):
clipboard.setText(text, QtGui.QClipboard.Clipboard)
clipboard.setText(text, QtGui.QClipboard.Selection)
def add_action(widget, text, fn, *shortcuts):
action = QtGui.QAction(text, widget)
action.connect(action, SIGNAL('triggered()'), fn)
if shortcuts:
shortcuts = list(set(shortcuts))
action.setShortcuts(shortcuts)
action.setShortcutContext(Qt.WidgetWithChildrenShortcut)
widget.addAction(action)
return action
def set_selected_item(widget, idx):
"""Sets a the currently selected item to the item at index idx."""
if type(widget) is QtGui.QTreeWidget:
@@ -192,20 +307,28 @@ def set_selected_item(widget, idx):
widget.setItemSelected(item, True)
widget.setCurrentItem(item)
def add_items(widget, items):
"""Adds items to a widget."""
for item in items:
widget.addItem(item)
def set_items(widget, items):
"""Clear the existing widget contents and set the new items."""
widget.clear()
add_items(widget, items)
@memoize
def tr(txt):
"""Translate a string into a local language."""
if type(txt) is QtCore.QString:
# This has already been translated; leave as-is
return unicode(txt)
return unicode(QtGui.QApplication.instance().translate('', txt))
def icon_file(filename, staged=False, untracked=False):
"""Returns a file path representing a corresponding file path."""
if staged:
@@ -216,14 +339,16 @@ def icon_file(filename, staged=False, untracked=False):
elif untracked:
ifile = resources.icon('untracked.png')
else:
ifile = utils.file_icon(filename)
ifile = utils.file_icon(core.encode(filename))
return ifile
def icon_for_file(filename, staged=False, untracked=False):
"""Returns a QIcon for a particular file path."""
ifile = icon_file(filename, staged=staged, untracked=untracked)
return icon(ifile)
def create_treeitem(filename, staged=False, untracked=False, check=True):
"""Given a filename, return a QListWidgetItem suitable
for adding to a QListWidget. "staged" and "untracked"
@@ -236,7 +361,7 @@ def create_treeitem(filename, staged=False, untracked=False, check=True):
def update_file_icons(widget, items, staged=True,
untracked=False, offset=0):
untracked=False, offset=0):
"""Populate a QListWidget with custom icon items."""
for idx, model_item in enumerate(items):
item = widget.item(idx+offset)
@@ -248,44 +373,104 @@ def set_listwidget_strings(widget, items):
widget.clear()
add_items(widget, [ QtGui.QListWidgetItem(i) for i in items ])
_icon_cache = {}
@memoize
def cached_icon(key):
"""Maintain a cache of standard icons and return cache entries."""
if key not in _icon_cache:
style = QtGui.QApplication.instance().style()
_icon_cache[key] = style.standardIcon(key)
return _icon_cache[key]
style = QtGui.QApplication.instance().style()
return style.standardIcon(key)
def dir_icon():
"""Return a standard icon for a directory."""
return cached_icon(QtGui.QStyle.SP_DirIcon)
def file_icon():
"""Return a standard icon for a file."""
return cached_icon(QtGui.QStyle.SP_FileIcon)
def diff_font():
"""Return the diff font string."""
qfont = QtGui.QFont()
font = cola.model().cola_config('fontdiff')
if font and qfont.fromString(font):
return qfont
family = 'Monospace'
if cola.utils.is_darwin():
family = 'Monaco'
qfont.setFamily(family)
font = unicode(qfont.toString())
# TODO this might not be the best place for the default
cola.model().set_diff_font(font)
return qfont
def set_diff_font(widget):
"""Updates the diff font based on the configured value."""
qfont = diff_font()
block = widget.signalsBlocked()
widget.blockSignals(True)
if isinstance(widget, QtGui.QFontComboBox):
widget.setCurrentFont(qfont)
else:
widget.setFont(qfont)
widget.blockSignals(block)
def apply_icon():
"""Return a standard Apply icon"""
return cached_icon(QtGui.QStyle.SP_DialogApplyButton)
def save_icon():
"""Return a standard Save icon"""
return cached_icon(QtGui.QStyle.SP_DialogSaveButton)
def ok_icon():
"""Return a standard Ok icon"""
return cached_icon(QtGui.QStyle.SP_DialogOkButton)
def discard_icon():
"""Return a standard Discard icon"""
return cached_icon(QtGui.QStyle.SP_DialogDiscardButton)
def close_icon():
"""Return a standard Close icon"""
return cached_icon(QtGui.QStyle.SP_DialogCloseButton)
def add_close_action(widget):
"""Adds close action and shortcuts to a widget."""
return add_action(widget, 'Close...',
widget.close, QtGui.QKeySequence.Close, 'Ctrl+Q')
def center_on_screen(widget):
"""Move widget to the center of the default screen"""
desktop = QtGui.QApplication.instance().desktop()
rect = desktop.screenGeometry(QtGui.QCursor().pos())
cy = rect.height()/2
cx = rect.width()/2
widget.move(cx - widget.width()/2, cy - widget.height()/2)
def save_state(widget):
if gitcfg.instance().get('cola.savewindowsettings', True):
settings.SettingsManager.save_gui_state(widget)
def export_window_state(widget, state, version):
# Save the window state
windowstate = widget.saveState(version)
state['windowstate'] = unicode(windowstate.toBase64().data())
return state
def apply_window_state(widget, state, version):
# Restore the dockwidget, etc. window state
try:
windowstate = state['windowstate']
widget.restoreState(QtCore.QByteArray.fromBase64(str(windowstate)),
version)
except KeyError:
pass
def apply_state(widget):
state = settings.SettingsManager.gui_state(widget)
widget.apply_state(state)
return bool(state)
@memoize
def theme_icon(name):
"""Grab an icon from the current theme with a fallback
Support older versions of Qt by catching AttributeError and
falling back to our default icons.
"""
try:
base, ext = os.path.splitext(name)
qicon = QtGui.QIcon.fromTheme(base)
if not qicon.isNull():
return qicon
except AttributeError:
pass
return icon(name)
+20 -25
View File
@@ -13,42 +13,37 @@ else:
# __file__ = '$prefix/cola/__file__.py'
_prefix = os.path.dirname(os.path.dirname(_modpath))
def prefix(*args):
"""Returns a path relative to cola's installation prefix"""
"""Return a path relative to cola's installation prefix"""
return os.path.join(_prefix, *args)
def doc(*args):
"""Returns a path relative to cola's /usr/share/doc/ directory"""
"""Return a path relative to cola's /usr/share/doc/ directory"""
return os.path.join(_prefix, 'share', 'doc', 'git-cola', *args)
def html_docs():
"""Returns the path to the cola html documentation."""
return doc('git-cola.html')
"""Return the path to the cola html documentation."""
# index.html only exists after the install-docs target is run,
# so fallback to git-cola.txt.
htmldocs = doc('html', 'index.html')
if os.path.exists(htmldocs):
return htmldocs
return doc('git-cola.txt')
def share(*args):
"""Returns a path relative to cola's /usr/share/ directory"""
"""Return a path relative to cola's /usr/share/ directory"""
return prefix('share', 'git-cola', *args)
def icon(basename):
"""Returns the full path to an icon file given a basename."""
return share('icons', basename)
"""Return the full path to an icon file given a basename."""
return 'icons:'+basename
def qm(name):
"""Returns the path to a qm file given its name"""
return share('qm', name + '.qm')
def stylesheet(name):
"""Returns a path relative to cola's /usr/share/../styles directory"""
stylesheet = share('styles', name + '.qss')
if os.path.exists(stylesheet):
return stylesheet
else:
return None
def style_dir():
"""Returns the path to the style dir within the cola install tree."""
return share('styles')
def resource_dirs(path):
"""Returns directories beneath a specific path"""
return [p for p in glob.glob(os.path.join(path, '*')) if os.path.isdir(p)]
def icon_dir():
"""Return the path to the style dir within the cola install tree."""
return share('icons')
+51
View File
@@ -0,0 +1,51 @@
"""Provides a serializer for arbitrary Python objects"""
import jsonpickle
jsonpickle.set_encoder_options('simplejson', indent=4)
from cola import utils
handlers = {}
def save(obj, path):
utils.write(path, encode(obj))
def load(path):
return decode(utils.slurp(path))
def clone(obj):
# Go in and out of encode/decode to return a clone
return decode(encode(obj))
def encode(obj):
handler = _gethandler(obj)
if handler:
handler.pre_encode_hook()
jsonstr = jsonpickle.encode(obj)
if handler:
handler.post_encode_hook()
return jsonstr
def decode(jsonstr):
obj = jsonpickle.decode(jsonstr)
handler = _gethandler(obj)
if handler:
handler.post_decode_hook()
return obj
def _gethandler(obj):
cls = type(obj)
# Allow base classes to provide a serialization handlers
# for their subclasses
if hasattr(cls, 'mro'):
for supercls in cls.mro():
if supercls in handlers:
return handlers[supercls](obj)
if cls in handlers:
return handlers[cls](obj)
return None
+23 -20
View File
@@ -3,9 +3,11 @@
"""
import os
import user
from cola.models.observable import ObservableModel
from cola import core
from cola import serializer
from cola.obsmodel import ObservableModel
class SettingsModel(ObservableModel):
def __init__(self):
@@ -13,22 +15,6 @@ class SettingsModel(ObservableModel):
ObservableModel.__init__(self)
self.bookmarks = []
self.gui_state = {}
self.load()
def path(self):
"""Path to the model's on-disk representation"""
return os.path.join(user.home, '.cola')
def load(self):
"""Loads settings if they exist"""
settings = self.path()
if os.path.exists(settings):
ObservableModel.load(self, settings)
def save(self):
"""Saves settings to the .cola file"""
# Call the base method
ObservableModel.save(self, self.path())
def add_bookmark(self, bookmark):
"""Adds a bookmark to the saved settings"""
@@ -40,14 +26,26 @@ class SettingsModel(ObservableModel):
if bookmark in self.bookmarks:
self.bookmarks.remove(bookmark)
class SettingsManager(object):
"""Manages a SettingsModel singleton
"""
_settings = SettingsModel()
_settings = None
# Here we store settings
_rcfile = os.path.join(core.decode(os.path.expanduser('~')), '.cola')
@staticmethod
def settings():
"""Returns the SettingsModel singleton"""
if not SettingsManager._settings:
if os.path.exists(SettingsManager._rcfile):
try:
SettingsManager._settings = serializer.load(SettingsManager._rcfile)
except: # bad json
SettingsManager._settings = SettingsModel()
else:
SettingsManager._settings = SettingsModel()
return SettingsManager._settings
@staticmethod
@@ -57,9 +55,14 @@ class SettingsManager(object):
state = gui.export_state()
model = SettingsManager.settings()
model.gui_state[name] = state
model.save()
SettingsManager.save()
@staticmethod
def gui_state(gui):
"""Returns the state for a gui"""
return SettingsManager.settings().gui_state.get(gui.name(), {})
@staticmethod
def save():
model = SettingsManager.settings()
serializer.save(model, SettingsManager._rcfile)
+62 -66
View File
@@ -1,66 +1,62 @@
from PyQt4.QtCore import SIGNAL
_signals = dict(add_signoff = SIGNAL('add_signoff'),
amend = SIGNAL('amend'),
amend_mode = SIGNAL('amend_mode'),
apply_diff_selection = SIGNAL('apply_diff_selection'),
branch_mode = SIGNAL('branch_mode'),
commit = SIGNAL('commit'),
edit = SIGNAL('edit'),
checkout = SIGNAL('checkout'),
checkout_branch = SIGNAL('checkout_branch'),
cherry_pick = SIGNAL('cherry_pick'),
clone = SIGNAL('clone'),
delete = SIGNAL('delete'),
delete_branch = SIGNAL('delete_branch'),
diff = SIGNAL('diff'),
diff_expr_mode = SIGNAL('diff_expr_mode'),
diff_mode = SIGNAL('diff_mode'),
diff_staged = SIGNAL('diff_staged'),
diff_text = SIGNAL('diff_text'),
diffstat = SIGNAL('diffstat'),
difftool = SIGNAL('difftool'),
editor_text = SIGNAL('editor_text'),
format_patch = SIGNAL('format_patch'),
grep = SIGNAL('grep'),
information = SIGNAL('information'),
inotify = SIGNAL('inotify'),
log_cmd = SIGNAL('log_cmd'),
load_commit_message = SIGNAL('load_commit_message'),
mergetool = SIGNAL('mergetool'),
mode = SIGNAL('mode'),
modified_summary = SIGNAL('modified_summary'),
open_repo = SIGNAL('open_repo'),
redo = SIGNAL('redo'),
rescan = SIGNAL('rescan'),
reset_mode = SIGNAL('reset_mode'),
review_branch_mode = SIGNAL('review_branch_mode'),
show_untracked = SIGNAL('show_untracked'),
stage = SIGNAL('stage'),
stage_diffs = SIGNAL('stage_diffs'),
stage_modified = SIGNAL('stage_modified'),
stage_untracked = SIGNAL('stage_untracked'),
staged_summary = SIGNAL('staged_summary'),
tag = SIGNAL('tag'),
unmerged_summary = SIGNAL('unmerged_summary'),
undo = SIGNAL('undo'),
undo_diffs = SIGNAL('undo_diffs'),
unstage = SIGNAL('unstage'),
unstage_diffs = SIGNAL('unstage_diffs'),
unstage_all = SIGNAL('unstage_all'),
unstage_selected = SIGNAL('unstage_selected'),
untracked_summary = SIGNAL('untracked_summary'),
visualize_all = SIGNAL('visualize_all'),
visualize_current = SIGNAL('visualize_current'),
visualize_paths = SIGNAL('visualize_paths'))
_signals_names = {}
for name, signal in _signals.iteritems():
_signals_names[signal] = name
# Bring signals into the module namespace
globals().update(_signals)
def name(signal):
"""Return the name for a signal."""
return _signals_names.get(signal, 'no-name')
add_signoff = 'add_signoff'
amend = 'amend'
amend_mode = 'amend_mode'
apply_diff_selection = 'apply_diff_selection'
apply_patches = 'apply_patches'
branch_mode = 'branch_mode'
commit = 'commit'
commits_selected = 'commits_selected'
edit = 'edit'
checkout = 'checkout'
checkout_branch = 'checkout_branch'
cherry_pick = 'cherry_pick'
clone = 'clone'
delete = 'delete'
ignore = 'ignore'
delete_branch = 'delete_branch'
diff = 'diff'
diff_expr_mode = 'diff_expr_mode'
diff_mode = 'diff_mode'
diff_staged = 'diff_staged'
diffstat = 'diffstat'
difftool = 'difftool'
format_patch = 'format_patch'
grep = 'grep'
information = 'information'
log_cmd = 'log_cmd'
load_commit_message = 'load_commit_message'
load_commit_template = 'load_commit_template'
load_previous_message = 'load_previous_message'
mergetool = 'mergetool'
mode = 'mode'
modified_summary = 'modified_summary'
open_repo = 'open_repo'
question = 'question'
redo = 'redo'
rescan = 'rescan'
reset_mode = 'reset_mode'
review_branch_mode = 'review_branch_mode'
run_config_action = 'run_config_action'
run_command = 'run_command'
set_diff_text = 'set_diff_text'
show_untracked = 'show_untracked'
signoff = 'signoff'
stage = 'stage'
stage_diffs = 'stage_diffs'
stage_modified = 'stage_modified'
stage_untracked = 'stage_untracked'
staged_summary = 'staged_summary'
tag = 'tag'
unmerged_summary = 'unmerged_summary'
undo = 'undo'
undo_diffs = 'undo_diffs'
unstage = 'unstage'
unstage_diffs = 'unstage_diffs'
unstage_all = 'unstage_all'
unstage_selected = 'unstage_selected'
untracked_summary = 'untracked_summary'
update_status = 'update_status'
visualize_all = 'visualize_all'
visualize_current = 'visualize_current'
visualize_paths = 'visualize_paths'
visualize_revision = 'visualize_revision'
+28
View File
@@ -0,0 +1,28 @@
from PyQt4 import QtGui
from cola.stash.controller import StashController
from cola.stash.model import StashModel
from cola.stash.view import StashView
def stash(parent=None):
"""Launches a stash dialog using the provided model + view
"""
if parent is None:
parent = QtGui.QApplication.instance().activeWindow()
model = StashModel()
view = StashView(model, parent)
ctl = StashController(model, view)
view.show()
view.raise_()
return ctl
if __name__ == '__main__':
from cola.app import ColaApplication
import cola
cola.model().update_status()
app = ColaApplication([])
ctl = stash()
app.exec_()
+9
View File
@@ -0,0 +1,9 @@
"""This controller handles the stash dialog."""
from cola.ctrl import Controller
from cola.stash.model import command_directory
class StashController(Controller):
def __init__(self, model, view):
Controller.__init__(self, model, view)
self.add_commands(command_directory)
+83
View File
@@ -0,0 +1,83 @@
import cola
from cola import observable
from cola import signals
from cola.git import git
from cola.cmds import BaseCommand, Rescan, rescan
apply_stash = 'apply_stash'
drop_stash = 'drop_stash'
save_stash = 'save_stash'
class StashModel(observable.Observable):
def __init__(self):
observable.Observable.__init__(self)
def stash_list(self):
return git.stash('list').splitlines()
def has_stashable_changes(self):
model = cola.model()
return bool(model.modified + model.staged)
def stash_info(self, revids=False, names=False):
"""Parses "git stash list" and returns a list of stashes."""
stashes = self.stash_list()
revids = [s[:s.index(':')] for s in stashes]
names = [s.split(': ', 2)[-1] for s in stashes]
return stashes, revids, names
def stash_diff(self, rev):
diffstat = git.stash('show', rev)
diff = git.stash('show', '-p', rev)
return diffstat + '\n\n' + diff
class ApplyStash(BaseCommand):
def __init__(self, selection, index):
BaseCommand.__init__(self)
self.selection = selection
self.index = index
def do(self):
if self.index:
args = ['apply', '--index', self.selection]
else:
args = ['apply', self.selection]
status, output = git.stash(with_stderr=True, with_status=True, *args)
cola.notifier().broadcast(signals.log_cmd, status, output)
class DropStash(BaseCommand):
def __init__(self, stash_sha1):
BaseCommand.__init__(self)
self.stash_sha1 = stash_sha1
def do(self):
status, output = git.stash('drop', self.stash_sha1,
with_stderr=True, with_status=True)
cola.notifier().broadcast(signals.log_cmd, status, output)
class SaveStash(BaseCommand):
def __init__(self, stash_name, keep_index):
BaseCommand.__init__(self)
self.stash_name = stash_name
self.keep_index = keep_index
def do(self):
if self.keep_index:
args = ['save', '--keep-index', self.stash_name]
else:
args = ['save', self.stash_name]
status, output = git.stash(with_stderr=True, with_status=True, *args)
cola.notifier().broadcast(signals.log_cmd, status, output)
command_directory = {
apply_stash: ApplyStash,
drop_stash: DropStash,
rescan: Rescan,
save_stash: SaveStash,
}
+206
View File
@@ -0,0 +1,206 @@
"""Provides the StashView dialog."""
from PyQt4 import QtCore
from PyQt4 import QtGui
from PyQt4.QtCore import SIGNAL
from cola import qt
from cola import qtutils
from cola import utils
from cola.stash.model import save_stash, apply_stash, drop_stash, rescan
from cola.views import standard
from cola.widgets.diff import DiffView
class StashView(standard.Dialog):
def __init__(self, model, parent=None):
standard.Dialog.__init__(self, parent=parent)
self.model = model
self.stashes = []
self.revids = []
self.names = []
self.setWindowModality(QtCore.Qt.WindowModal)
self.setWindowTitle(self.tr('Stash'))
if parent:
self.resize(parent.width(), 420)
else:
self.resize(700, 420)
self.stash_list = QtGui.QListWidget(self)
self.stash_view = DiffView(self)
self.button_apply =\
self.toolbutton(self.tr('Apply'),
self.tr('Apply the selected stash'),
qtutils.apply_icon())
self.button_save =\
self.toolbutton(self.tr('Save'),
self.tr('Save modified state to new stash'),
qtutils.save_icon())
self.button_remove = \
self.toolbutton(self.tr('Remove'),
self.tr('Remove the selected stash'),
qtutils.discard_icon())
self.button_close = \
self.pushbutton(self.tr('Close'),
self.tr('Close'), qtutils.close_icon())
self.keep_index = QtGui.QCheckBox(self)
self.keep_index.setText(self.tr('Keep Index'))
self.keep_index.setChecked(True)
self.setTabOrder(self.button_save, self.button_apply)
self.setTabOrder(self.button_apply, self.button_remove)
self.setTabOrder(self.button_remove, self.keep_index)
self.setTabOrder(self.keep_index, self.button_close)
# Arrange layouts
self.main_layt = QtGui.QVBoxLayout()
self.main_layt.setMargin(6)
self.main_layt.setSpacing(6)
self.btn_layt = QtGui.QHBoxLayout()
self.btn_layt.setMargin(0)
self.btn_layt.setSpacing(4)
self.splitter = QtGui.QSplitter()
self.splitter.setOrientation(QtCore.Qt.Horizontal)
self.splitter.setChildrenCollapsible(True)
self.splitter.setStretchFactor(0, 1)
self.splitter.setStretchFactor(1, 1)
self.splitter.insertWidget(0, self.stash_list)
self.splitter.insertWidget(1, self.stash_view)
self.btn_layt.addWidget(self.button_save)
self.btn_layt.addWidget(self.button_apply)
self.btn_layt.addWidget(self.button_remove)
self.btn_layt.addWidget(self.keep_index)
self.btn_layt.addStretch()
self.btn_layt.addWidget(self.button_close)
self.main_layt.addWidget(self.splitter)
self.main_layt.addLayout(self.btn_layt)
self.setLayout(self.main_layt)
self.splitter.setSizes([self.width()/3, self.width()*2/3])
self.connect(self.button_apply, SIGNAL('clicked()'),
self.stash_apply)
self.connect(self.button_save, SIGNAL('clicked()'),
self.stash_save)
self.connect(self.button_remove, SIGNAL('clicked()'),
self.stash_remove)
self.connect(self.button_close, SIGNAL('clicked()'), self.close)
self.update_from_model()
self.update_actions()
self.connect(self.stash_list, SIGNAL('itemSelectionChanged()'),
self.item_selected)
def close(self):
self.accept()
self.emit(SIGNAL(rescan))
def toolbutton(self, text, tooltip, icon):
return qt.create_toolbutton(self,
text=text, tooltip=tooltip, icon=icon)
def pushbutton(self, text, tooltip, icon):
btn = QtGui.QPushButton(self)
btn.setText(self.tr(text))
btn.setToolTip(self.tr(tooltip))
btn.setIcon(icon)
return btn
def selected_stash(self):
"""Returns the stash name of the currently selected stash
"""
list_widget = self.stash_list
stash_list = self.revids
return qtutils.selected_item(list_widget, stash_list)
def selected_name(self):
list_widget = self.stash_list
stash_list = self.names
return qtutils.selected_item(list_widget, stash_list)
def item_selected(self):
"""Shows the current stash in the main view."""
self.update_actions()
selection = self.selected_stash()
if not selection:
return
diff_text = self.model.stash_diff(selection)
self.stash_view.setPlainText(diff_text)
def update_actions(self):
has_changes = self.model.has_stashable_changes()
has_stash = bool(self.selected_stash())
self.button_save.setEnabled(has_changes)
self.button_apply.setEnabled(has_stash)
self.button_remove.setEnabled(has_stash)
def update_from_model(self):
"""Initiates git queries on the model and updates the view
"""
stashes, revids, names = self.model.stash_info()
self.stashes = stashes
self.revids = revids
self.names = names
self.stash_list.clear()
self.stash_list.addItems(self.stashes)
def stash_apply(self):
"""Applies the currently selected stash
"""
selection = self.selected_stash()
if not selection:
return
index = self.keep_index.isChecked()
self.emit(SIGNAL(apply_stash), selection, index)
self.accept()
self.emit(SIGNAL(rescan))
def stash_save(self):
"""Saves the worktree in a stash
This prompts the user for a stash name and creates
a git stash named accordingly.
"""
stash_name, ok = qtutils.prompt('Save Stash',
'Enter a name for the stash')
if not ok or not stash_name:
return
# Sanitize the stash name
stash_name = utils.sanitize(stash_name)
if stash_name in self.names:
qtutils.critical('Oops!',
'A stash named "%s" already exists' % stash_name)
return
keep_index = self.keep_index.isChecked()
self.emit(SIGNAL(save_stash), stash_name, keep_index)
self.accept()
self.emit(SIGNAL(rescan))
def stash_remove(self):
"""Drops the currently selected stash
"""
selection = self.selected_stash()
name = self.selected_name()
if not selection:
return
if not qtutils.confirm(self,
'Remove Stash?',
'Remove "%s"?' % name,
'Recovering these changes may not be possible.',
'Remove',
icon=qtutils.discard_icon()):
return
self.emit(SIGNAL(drop_stash), selection)
self.update_from_model()
self.stash_view.setPlainText('')
+154 -87
View File
@@ -6,17 +6,16 @@ import re
import sys
import errno
import platform
import shlex
import subprocess
import hashlib
import mimetypes
from glob import glob
from cStringIO import StringIO
from cola import git
from cola import core
from cola import resources
from cola.git import shell_quote
from cola.compat import hashlib
from cola.decorators import memoize
KNOWN_FILE_MIME_TYPES = {
'text': 'script.png',
@@ -64,15 +63,6 @@ def run_cmd(command):
return git.Git.execute(command)
def qm_for_locale(locale):
"""Returns the .qm file for a particular $LANG values."""
regex = re.compile(r'([^\.])+\..*$')
match = regex.match(locale)
if match:
locale = match.group(1)
return resources.qm(locale.split('_')[0])
def ident_file_type(filename):
"""Returns an icon based on the contents of filename."""
if os.path.exists(filename):
@@ -100,62 +90,10 @@ def file_icon(filename):
return resources.icon(ident_file_type(filename))
def win32_abspath(exe):
"""Return the absolute path to an .exe if it exists"""
if os.path.exists(exe):
return exe
if not exe.endswith('.exe'):
exe += '.exe'
if os.path.exists(exe):
return exe
for path in os.environ['PATH'].split(os.pathsep):
abspath = os.path.join(path, exe)
if os.path.exists(abspath):
return abspath
return None
def win32_expand_paths(args):
"""Expand filenames after the double-dash"""
if '--' not in args:
return args
dashes_idx = args.index('--')
cmd = args[:dashes_idx+1]
for path in args[dashes_idx+1:]:
cmd.append(shell_quote(os.path.join(os.getcwd(), path)))
return cmd
def fork(args):
"""Launch a command in the background."""
if os.name in ('nt', 'dos'):
# Windows is absolutely insane.
#
# If we want to launch 'gitk' we have to use the 'sh -c' trick.
#
# If we want to launch 'git.exe' we have to expand all filenames
# after the double-dash.
#
# os.spawnv wants an absolute path in the command name but not in
# the command vector. Wow.
enc_args = win32_expand_paths([core.encode(a) for a in args])
abspath = win32_abspath(enc_args[0])
if abspath:
# e.g. fork(['git', 'difftool', '--no-prompt', '--', 'path'])
return os.spawnv(os.P_NOWAIT, abspath, enc_args)
# e.g. fork(['gitk', '--all'])
sh_exe = win32_abspath('sh')
enc_argv = map(shell_quote, enc_args)
cmdstr = ' '.join(enc_argv)
cmd = ['sh.exe', '-c', cmdstr]
return os.spawnv(os.P_NOWAIT, sh_exe, cmd)
else:
# Unix is absolutely simple
enc_args = [core.encode(a) for a in args]
enc_argv = map(shell_quote, enc_args)
cmdstr = ' '.join(enc_argv)
return os.system(cmdstr + '&')
encoded_args = [core.encode(arg) for arg in args]
return subprocess.Popen(encoded_args).pid
def sublist(a,b):
@@ -235,18 +173,19 @@ def dirname(path):
def slurp(path):
"""Slurps a filepath into a string."""
fh = open(path)
fh = open(core.encode(path))
slushy = core.read_nointr(fh)
fh.close()
return core.decode(slushy)
def write(path, contents):
"""Writes a string to a file."""
fh = open(path, 'w')
"""Writes a raw string to a file."""
fh = open(core.encode(path), 'wb')
core.write_nointr(fh, core.encode(contents))
fh.close()
def strip_prefix(prefix, string):
"""Return string, without the prefix. Blow up if string doesn't
start with prefix."""
@@ -259,6 +198,20 @@ def sanitize(s):
s = s.replace(c, '_')
return s
def shell_split(s):
"""Split string apart into utf-8 encoded words using shell syntax"""
try:
return shlex.split(core.encode(s))
except ValueError:
return [s]
def shell_usplit(s):
"""Returns a unicode list instead of encoded strings"""
return [core.decode(arg) for arg in shell_split(s)]
def is_linux():
"""Is this a linux machine?"""
while True:
@@ -288,17 +241,32 @@ def is_darwin():
return 'macintosh' in p or 'darwin' in p
def is_broken():
"""Is it windows or mac? (e.g. is running git-mergetool non-trivial?)"""
if is_darwin():
return True
while True:
try:
return platform.system() == 'Windows'
except IOError, e:
if e.errno == errno.EINTR:
continue
raise e
@memoize
def is_win32():
"""Return True on win32"""
return os.name in ('nt', 'dos')
def win32_set_binary(fd):
try:
import msvcrt
except ImportError:
return
# When run without console, pipes may expose invalid
# fileno(), usually set to -1.
if hasattr(fd, 'fileno') and fd.fileno() >= 0:
msvcrt.setmode(fd.fileno(), os.O_BINARY)
def posix_set_binary(fd):
"""POSIX file descriptors are always binary"""
pass
if is_win32():
set_binary = win32_set_binary
else:
set_binary = posix_set_binary
def checksum(path):
@@ -308,8 +276,107 @@ def checksum(path):
return md5.hexdigest()
def quote_repopath(repopath):
"""Quote a path for nt/dos only."""
if os.name in ('nt', 'dos'):
repopath = '"%s"' % repopath
return repopath
def error(msg, *args):
"""Print an error message to stderr."""
print >> sys.stderr, "ERROR:", msg % args
def warn(msg, *args):
"""Print a warning message to stderr."""
print >> sys.stderr, "warning:", msg % args
def die(msg, *args):
"""Print as error message to stderr and exit the program."""
error(msg, *args)
sys.exit(1)
class ProgressIndicator(object):
"""Simple progress indicator.
Displayed as a spinning character by default, but can be customized
by passing custom messages that overrides the spinning character.
"""
States = ("|", "/", "-", "\\")
def __init__(self, prefix="", f=sys.stdout):
"""Create a new ProgressIndicator, bound to the given file object."""
self.n = 0 # Simple progress counter
self.f = f # Progress is written to this file object
self.prev_len = 0 # Length of previous msg (to be overwritten)
self.prefix = prefix # Prefix prepended to each progress message
self.prefix_lens = [] # Stack of prefix string lengths
def pushprefix(self, prefix):
"""Append the given prefix onto the prefix stack."""
self.prefix_lens.append(len(self.prefix))
self.prefix += prefix
def popprefix(self):
"""Remove the last prefix from the prefix stack."""
prev_len = self.prefix_lens.pop()
self.prefix = self.prefix[:prev_len]
def __call__(self, msg = None, lf=False):
"""Indicate progress, possibly with a custom message."""
if msg is None:
msg = self.States[self.n % len(self.States)]
msg = self.prefix + msg
print >> self.f, "\r%-*s" % (self.prev_len, msg),
self.prev_len = len(msg.expandtabs())
if lf:
print >> self.f
self.prev_len = 0
self.n += 1
def finish (self, msg="done", noprefix=False):
"""Finalize progress indication with the given message."""
if noprefix:
self.prefix = ""
self(msg, True)
def start_command(args, cwd=None, shell=False, add_env=None,
stdin=subprocess.PIPE, stdout=subprocess.PIPE,
stderr=subprocess.PIPE):
"""Start the given command, and return a subprocess object.
This provides a simpler interface to the subprocess module.
"""
env = None
if add_env is not None:
env = os.environ.copy()
env.update(add_env)
return subprocess.Popen(args, bufsize=1, stdin=stdin, stdout=stdout,
stderr=stderr, cwd=cwd, shell=shell,
env=env, universal_newlines=True)
def run_command(args, cwd=None, shell=False, add_env=None,
flag_error=True):
"""Run the given command to completion, and return its results.
This provides a simpler interface to the subprocess module.
The results are formatted as a 3-tuple: (exit_code, output, errors)
If flag_error is enabled, Error messages will be produced if the
subprocess terminated with a non-zero exit code and/or stderr
output.
The other arguments are passed on to start_command().
"""
process = start_command(args, cwd, shell, add_env)
(output, errors) = process.communicate()
exit_code = process.returncode
if flag_error and errors:
error("'%s' returned errors:\n---\n%s---", " ".join(args), errors)
if flag_error and exit_code:
error("'%s' returned exit code %i", " ".join(args), exit_code)
return (exit_code, output, errors)
+59 -32
View File
@@ -1,32 +1,40 @@
# Copyright (c) 2008 David Aguilar
"""Provides the current cola version number"""
# Copyright (c) 2012 David Aguilar
"""Provide cola's version number"""
# The current git-cola version
_default_version = '1.7.0'
import re
import os
import sys
if __name__ == '__main__':
srcdir = os.path.dirname(os.path.dirname(__file__))
sys.path.insert(1, srcdir)
sys.path.insert(1, os.path.join(srcdir, 'thirdparty'))
from cola import git
from cola import errors
from cola import utils
from cola import resources
from cola.decorators import memoize
# minimum version requirements
_versions = {
'git': '1.5.2',
'python': '2.4',
'pyqt': '4.3',
# git-difftool moved out of contrib in git 1.6.3
'difftool-builtin': '1.6.3',
# git-diff learned --patience in 1.6.2
# git-mergetool learned --no-prompt in 1.6.2
'mergetool-no-prompt': '1.6.2',
# git-difftool moved out of contrib in git 1.6.3
'git': '1.6.3',
'python': '2.4',
'pyqt': '4.4',
'pyqt_qrunnable': '4.4',
'diff-submodule': '1.6.6',
}
def get(key):
"""Returns an entry from the known versions table"""
return _versions.get(key)
# cached to avoid recalculation
_version = None
class VersionUnavailable(Exception):
pass
@@ -43,7 +51,7 @@ def git_describe_version():
with_stderr=True)
except errors.GitCommandError, e:
raise VersionUnavailable(str(e))
if not re.match(r'^v[0-9]', v):
if v[0:1] != 'v' or not v[1:2].isdigit():
raise VersionUnavailable('%s: bad version' % v)
try:
dirty = git.Git.execute(['git', 'diff-index', '--name-only', 'HEAD'])
@@ -51,7 +59,7 @@ def git_describe_version():
raise VersionUnavailable(str(e))
if dirty:
v += '-dirty'
return re.sub('-', '.', utils.strip_prefix('v', v))
return utils.strip_prefix('v', v.replace('-', '.'))
def builtin_version():
@@ -64,46 +72,59 @@ def builtin_version():
return bv.version
def _builtin_version_file(ext = 'py'):
@memoize
def _builtin_version_file(ext='py'):
"""Returns the path to cola's builtin_version.py."""
dirname = os.path.dirname(os.path.abspath(__file__))
dirname = os.path.dirname(__file__)
return os.path.join(dirname, 'builtin_version.%s' % ext)
def release_version():
"""Return a version number for a release
First see if there is a version file (included in release tarballs),
then try git-describe, then default.
"""
if os.path.isdir('.git'):
try:
return git_describe_version()
except VersionUnavailable:
pass
return version()
def write_builtin_version():
"""Writes cola/builtin_version.py."""
try:
v = git_describe_version()
except VersionUnavailable:
return
"""Writes cola/builtin_version.py
"""
v = release_version()
f = file(_builtin_version_file(), 'w')
f.write('# This file was generated automatically. Do not edit by hand.\n'
'version = %r\n' % v)
f.close()
def delete_builtin_version():
"""Deletes cola/builtin_version.py."""
for ext in ('py', 'pyc', 'pyo'):
fn = _builtin_version_file(ext)
fn = _builtin_version_file(ext=ext)
if os.path.exists(fn):
os.remove(fn)
def version():
@memoize
def version(vstr=_default_version):
"""Returns the builtin version or calculates the current version."""
global _version
if _version:
return _version
for v in [builtin_version, git_describe_version]:
try:
_version = v()
break
return v()
except VersionUnavailable:
pass
if not _version:
_version = 'unknown-version'
return _version
return vstr
@memoize
def check_version(min_ver, ver):
"""Check whether ver is greater or equal to min_ver
"""
@@ -112,6 +133,7 @@ def check_version(min_ver, ver):
return min_ver_list <= ver_list
@memoize
def check(key, ver):
"""Checks if a version is greater than the known version for <what>"""
return check_version(get(key), ver)
@@ -130,6 +152,11 @@ def version_to_list(version):
return ver_list
@memoize
def git_version():
"""Returns the current GIT version"""
return utils.run_cmd(['git', '--version']).split()[2]
return git.instance().version().split()[-1]
if __name__ == '__main__':
print(release_version())
+2 -3
View File
@@ -1,15 +1,14 @@
"""Provides the BookmarksView dialog."""
from PyQt4 import QtCore
from PyQt4 import QtGui
from PyQt4.QtCore import SIGNAL
from cola.views import standard
class BookmarkView(standard.StandardDialog):
class BookmarkView(standard.Dialog):
def __init__(self, parent=None):
standard.StandardDialog.__init__(self, parent=parent)
standard.Dialog.__init__(self, parent=parent)
self.setWindowTitle(self.tr('Bookmarks'))
self.resize(494, 238)
+6 -4
View File
@@ -7,9 +7,9 @@ from PyQt4.QtCore import SIGNAL
from cola.views import standard
class CompareView(standard.StandardDialog):
class CompareView(standard.Dialog):
def __init__(self, parent=None):
standard.StandardDialog.__init__(self, parent=parent)
standard.Dialog.__init__(self, parent=parent)
self.setWindowTitle(self.tr('Compare Commits'))
self.resize(649, 372)
@@ -83,9 +83,11 @@ class CompareView(standard.StandardDialog):
self._bottom_layt.addItem(self._bottom_spacer)
self.button_compare = QtGui.QPushButton(self)
self.button_compare.setText(self.tr('Compare'))
self._bottom_layt.addWidget(self.button_compare)
self.button_close = QtGui.QPushButton(self)
self.button_close.setText(self.tr('Close'))
self._bottom_layt.addWidget(self.button_close)
self._main_layt.addLayout(self._bottom_layt)
@@ -93,9 +95,9 @@ class CompareView(standard.StandardDialog):
self.connect(self.button_close, SIGNAL('clicked()'), self.accept)
class BranchCompareView(standard.StandardDialog):
class BranchCompareView(standard.Dialog):
def __init__(self, parent=None):
standard.StandardDialog.__init__(self, parent=parent)
standard.Dialog.__init__(self, parent=parent)
self.setWindowTitle(self.tr('Branch Diff Viewer'))
self.resize(658, 350)
+2 -3
View File
@@ -1,15 +1,14 @@
from PyQt4 import QtCore
from PyQt4 import QtGui
from PyQt4.QtCore import SIGNAL
from cola.views import standard
class CreateBranchView(standard.StandardDialog):
class CreateBranchView(standard.Dialog):
"""A dialog for creating branches."""
def __init__(self, parent=None):
standard.StandardDialog.__init__(self, parent=parent)
standard.Dialog.__init__(self, parent=parent)
self.setWindowTitle(self.tr('Create Branch'))
+5 -12
View File
@@ -1,18 +1,15 @@
import os
import sys
from PyQt4 import QtGui
from PyQt4 import QtCore
from PyQt4.QtCore import SIGNAL
import cola
from cola import qtutils
from cola.prefs import diff_font
from cola.views import standard
class CreateTag(standard.StandardDialog):
class CreateTag(standard.Dialog):
def __init__(self, parent=None):
standard.StandardDialog.__init__(self, parent=parent)
standard.Dialog.__init__(self, parent=parent)
self.setWindowModality(QtCore.Qt.WindowModal)
self.setWindowTitle(self.tr('Create Tag'))
self.resize(506, 295)
self._main_layt = QtGui.QVBoxLayout(self)
@@ -54,6 +51,7 @@ class CreateTag(standard.StandardDialog):
self.tag_msg = QtGui.QTextEdit(self)
self.tag_msg.setAcceptRichText(False)
self.tag_msg.setToolTip(self.tr('Specifies the tag message'))
self.tag_msg.setFont(diff_font())
self._input_form_layt.setWidget(2, QtGui.QFormLayout.FieldRole,
self.tag_msg)
# Revision
@@ -84,9 +82,4 @@ class CreateTag(standard.StandardDialog):
self.close_button.setText(self.tr('Close'))
self._button_hbox_layt.addWidget(self.close_button)
self.connect(self.sign_tag, SIGNAL('stateChanged(int)'),
self.tag_msg.setEnabled)
self.connect(self.close_button, SIGNAL('clicked()'), self.accept)
qtutils.set_diff_font(self.tag_msg)
self.tag_msg.setEnabled(False)
-696
View File
@@ -1,696 +0,0 @@
"""This view provides the main git-cola user interface.
"""
import os
from PyQt4 import QtGui
from PyQt4 import QtCore
from PyQt4.QtCore import Qt
from PyQt4.QtCore import SIGNAL
import cola
from cola import core
from cola import utils
from cola import qtutils
from cola import settings
from cola import signals
from cola import resources
from cola.qtutils import SLOT
from cola.views import about
from cola.views.syntax import DiffSyntaxHighlighter
from cola.views.mainwindow import MainWindow
from cola.controllers import compare
from cola.controllers import search as smod
from cola.controllers import createtag
from cola.controllers.bookmark import manage_bookmarks
from cola.controllers.bookmark import save_bookmark
from cola.controllers.createbranch import create_new_branch
from cola.controllers.merge import local_merge
from cola.controllers.merge import abort_merge
from cola.controllers.options import update_options
from cola.controllers.util import choose_from_combo
from cola.controllers.util import choose_from_list
from cola.controllers.remote import remote_action
from cola.controllers.repobrowser import browse_git_branch
from cola.controllers.stash import stash
from cola.controllers.selectcommits import select_commits
class MainView(MainWindow):
"""The main cola interface."""
idx_header = -1
idx_staged = 0
idx_modified = 1
idx_unmerged = 2
idx_untracked = 3
idx_end = 4
# Read-only mode property
mode = property(lambda self: self.model.mode)
def __init__(self, parent=None):
MainWindow.__init__(self, parent)
self.amend_is_checked = self.amend_checkbox.isChecked
# Qt does not support noun/verbs
self.commit_button.setText(qtutils.tr('Commit@@verb'))
self.commit_menu.setTitle(qtutils.tr('Commit@@verb'))
# Diff/patch syntax highlighter
self.syntax = DiffSyntaxHighlighter(self.display_text.document())
# Display the current column
self.connect(self.commitmsg,
SIGNAL('cursorPositionChanged()'),
self.show_cursor_position)
# Keeps track of merge messages we've seen
self.merge_message_hash = ''
# Initialize the seen tree widget indexes
self._seen_indexes = set()
# Initialize the GUI to show 'Column: 00'
self.show_cursor_position()
# Internal field used by import/export_state().
# Change this whenever dockwidgets are removed.
self._widget_version = 1
self.model = cola.model()
self.model.add_message_observer(self.model.message_updated,
self._update_view)
# Listen for text and amend messages
cola.notifier().listen(signals.diff_text, self.set_display)
cola.notifier().listen(signals.mode, self._mode_changed)
cola.notifier().listen(signals.inotify, self._inotify_enabled)
cola.notifier().listen(signals.amend, self.amend_checkbox.setChecked)
# Broadcast the amend mode
self.connect(self.amend_checkbox, SIGNAL('toggled(bool)'),
SLOT(signals.amend_mode))
# Add button callbacks
self._relay_button(self.alt_button, signals.reset_mode)
self._relay_button(self.rescan_button, signals.rescan)
self._relay_button(self.signoff_button, signals.add_signoff)
self._connect_button(self.stage_button, self.stage)
self._connect_button(self.unstage_button, self.unstage)
self._connect_button(self.commit_button, self.commit)
self._connect_button(self.fetch_button, self.fetch)
self._connect_button(self.push_button, self.push)
self._connect_button(self.pull_button, self.pull)
self._connect_button(self.stash_button, stash)
# Menu actions
actions = (
(self.menu_quit, self.close),
(self.menu_branch_compare, compare.branch_compare),
(self.menu_branch_diff, self.branch_diff),
(self.menu_branch_review, self.review_branch),
(self.menu_browse_branch, self.browse_current),
(self.menu_browse_other_branch, self.browse_other),
(self.menu_browse_commits, self.browse_commits),
(self.menu_create_tag, createtag.create_tag),
(self.menu_create_branch, create_new_branch),
(self.menu_checkout_branch, self.checkout_branch),
(self.menu_delete_branch, self.branch_delete),
(self.menu_rebase_branch, self.rebase),
(self.menu_clone_repo, self.clone_repo),
(self.menu_commit_compare, compare.compare),
(self.menu_commit_compare_file, compare.compare_file),
(self.menu_cherry_pick, self.cherry_pick),
(self.menu_diff_expression, self.diff_expression),
(self.menu_diff_branch, self.diff_branch),
(self.menu_export_patches, self.export_patches),
(self.menu_help_about, about.launch_about_dialog),
(self.menu_help_docs,
lambda: self.model.git.web__browse(resources.html_docs())),
(self.menu_load_commitmsg, self.load_commitmsg),
(self.menu_load_commitmsg_template, self.load_template),
(self.menu_manage_bookmarks, manage_bookmarks),
(self.menu_save_bookmark, save_bookmark),
(self.menu_merge_local, local_merge),
(self.menu_merge_abort, abort_merge),
(self.menu_open_repo, self.open_repo),
(self.menu_options, update_options),
(self.menu_rescan, SLOT(signals.rescan)),
(self.menu_search_grep, self.grep),
(self.menu_search_revision, smod.search(smod.REVISION_ID)),
(self.menu_search_revision_range, smod.search(smod.REVISION_RANGE)),
(self.menu_search_message, smod.search(smod.MESSAGE)),
(self.menu_search_path, smod.search(smod.PATH, True)),
(self.menu_search_date_range, smod.search(smod.DATE_RANGE)),
(self.menu_search_diff, smod.search(smod.DIFF)),
(self.menu_search_author, smod.search(smod.AUTHOR)),
(self.menu_search_committer, smod.search(smod.COMMITTER)),
(self.menu_show_diffstat, SLOT(signals.diffstat)),
(self.menu_stash, stash),
(self.menu_stage_modified, SLOT(signals.stage_modified)),
(self.menu_stage_untracked, SLOT(signals.stage_untracked)),
(self.menu_unstage_selected, SLOT(signals.unstage_selected)),
(self.menu_unstage_all, SLOT(signals.unstage_all)),
(self.menu_visualize_all, SLOT(signals.visualize_all)),
(self.menu_visualize_current, SLOT(signals.visualize_current)),
# TODO This edit menu stuff should/could be command objects
(self.menu_cut, self.action_cut),
(self.menu_copy, self.action_copy),
(self.menu_paste, self.commitmsg.paste),
(self.menu_delete, self.action_delete),
(self.menu_select_all, self.commitmsg.selectAll),
(self.menu_undo, self.commitmsg.undo),
(self.menu_redo, self.commitmsg.redo),
)
for menu, callback in actions:
self.connect(menu, SIGNAL('triggered()'), callback)
# Install diff shortcut keys for stage/unstage
self.display_text.keyPressEvent = self.diff_key_press_event
self.display_text.contextMenuEvent = self.diff_context_menu_event
# Restore saved settings
self._load_gui_state()
def _relay_button(self, button, signal):
callback = SLOT(signal)
self._connect_button(button, callback)
def _connect_button(self, button, callback):
self.connect(button, SIGNAL('clicked()'), callback)
def _inotify_enabled(self, enabled):
"""Hide the rescan button when inotify is enabled."""
if enabled:
self.rescan_button.hide()
else:
self.rescan_button.show()
def _update_view(self):
"""Update the title with the current branch and directory name."""
title = '%s [%s]' % (self.model.project,
self.model.currentbranch)
if self.mode in (self.model.mode_diff, self.model.mode_diff_expr):
title += ' *** diff mode***'
elif self.mode == self.model.mode_review:
title += ' *** review mode***'
elif self.mode == self.model.mode_amend:
title += ' *** amending ***'
self.setWindowTitle(title)
if self.mode != self.model.mode_amend:
self.amend_checkbox.blockSignals(True)
self.amend_checkbox.setChecked(False)
self.amend_checkbox.blockSignals(False)
if not self.model.read_only() and self.mode != self.model.mode_amend:
# Check if there's a message file in .git/
merge_msg_path = self.model.merge_message_path()
if merge_msg_path is None:
return
merge_msg_hash = utils.checksum(merge_msg_path)
if merge_msg_hash == self.merge_message_hash:
return
self.merge_message_hash = merge_msg_hash
cola.notifier().broadcast(signals.load_commit_message,
merge_msg_path)
def _mode_changed(self, mode):
"""React to mode changes; hide/show the "Exit Diff Mode" button."""
if mode in (self.model.mode_review,
self.model.mode_diff,
self.model.mode_diff_expr):
self.alt_button.setMinimumHeight(40)
self.alt_button.show()
else:
self.alt_button.setMinimumHeight(1)
self.alt_button.hide()
def set_display(self, text):
"""Set the diff text display."""
scrollbar = self.display_text.verticalScrollBar()
scrollvalue = scrollbar.value()
if text is not None:
self.display_text.setText(text)
scrollbar.setValue(scrollvalue)
def action_cut(self):
self.action_copy()
self.action_delete()
def action_copy(self):
cursor = self.commitmsg.textCursor()
selection = cursor.selection().toPlainText()
qtutils.set_clipboard(selection)
def action_delete(self):
self.commitmsg.textCursor().removeSelectedText()
def copy_display(self):
cursor = self.display_text.textCursor()
selection = cursor.selection().toPlainText()
qtutils.set_clipboard(selection)
def diff_selection(self):
cursor = self.display_text.textCursor()
offset = cursor.position()
selection = unicode(cursor.selection().toPlainText())
return offset, selection
def selected_line(self):
cursor = self.display_text.textCursor()
offset = cursor.position()
contents = unicode(self.display_text.toPlainText())
while (offset >= 1
and contents[offset-1]
and contents[offset-1] != '\n'):
offset -= 1
data = contents[offset:]
if '\n' in data:
line, rest = data.split('\n', 1)
else:
line = data
return line
def show_cursor_position(self):
"""Update the UI with the current row and column."""
cursor = self.commitmsg.textCursor()
position = cursor.position()
txt = unicode(self.commitmsg.toPlainText())
rows = txt[:position].count('\n') + 1
cols = cursor.columnNumber()
display = ' %d,%d ' % (rows, cols)
if cols > 78:
display = ('<span style="color: white; '
' background-color: red;"'
'>%s</span>' % display.replace(' ', '&nbsp;'))
elif cols > 72:
display = ('<span style="color: black; '
' background-color: orange;"'
'>%s</span>' % display.replace(' ', '&nbsp;'))
elif cols > 64:
display = ('<span style="color: black; '
' background-color: yellow;"'
'>%s</span>' % display.replace(' ', '&nbsp;'))
self.position_label.setText(display)
def import_state(self, state):
"""Imports data for save/restore"""
MainWindow.import_state(self, state)
# Restore the dockwidget, etc. window state
if 'windowstate' in state:
windowstate = state['windowstate']
self.restoreState(QtCore.QByteArray.fromBase64(str(windowstate)),
self._widget_version)
def export_state(self):
"""Exports data for save/restore"""
state = MainWindow.export_state(self)
# Save the window state
windowstate = self.saveState(self._widget_version)
state['windowstate'] = unicode(windowstate.toBase64().data())
return state
def review_branch(self):
"""Diff against an arbitrary revision, branch, tag, etc."""
branch = choose_from_combo('Select Branch, Tag, or Commit-ish',
self.model.all_branches() +
self.model.tags)
if not branch:
return
cola.notifier().broadcast(signals.review_branch_mode, branch)
def branch_diff(self):
"""Diff against an arbitrary revision, branch, tag, etc."""
branch = choose_from_combo('Select Branch, Tag, or Commit-ish',
['HEAD^'] +
self.model.all_branches() +
self.model.tags)
if not branch:
return
cola.notifier().broadcast(signals.diff_mode, branch)
def diff_expression(self):
"""Diff using an arbitrary expression."""
expr = choose_from_combo('Enter Diff Expression',
self.model.all_branches() +
self.model.tags)
if not expr:
return
cola.notifier().broadcast(signals.diff_expr_mode, expr)
def diff_branch(self):
"""Launches a diff against a branch."""
branch = choose_from_combo('Select Branch, Tag, or Commit-ish',
['HEAD^'] +
self.model.all_branches() +
self.model.tags)
if not branch:
return
zfiles_str = self.model.git.diff(branch, name_only=True,
no_color=True,
z=True).rstrip('\0')
files = zfiles_str.split('\0')
filename = choose_from_list('Select File', files)
if not filename:
return
cola.notifier().broadcast(signals.branch_mode, branch, filename)
def _load_gui_state(self):
"""Restores the gui from the preferences file."""
state = settings.SettingsManager.gui_state(self)
self.import_state(state)
def load_commitmsg(self):
"""Load a commit message from a file."""
filename = qtutils.open_dialog(self,
'Load Commit Message...',
self.model.getcwd())
if filename:
cola.notifier().broadcast(signals.load_commit_message, filename)
def load_template(self):
"""Load the configured commit message template."""
template = self.model.global_config('commit.template')
if template:
cola.notifier().broadcast(signals.load_commit_message, template)
def diff_key_press_event(self, event):
"""Handle shortcut keys in the diff view."""
if event.key() != QtCore.Qt.Key_H and event.key() != QtCore.Qt.Key_S:
event.ignore()
return
staged, modified, unmerged, untracked = cola.single_selection()
if event.key() == QtCore.Qt.Key_H:
if self.mode == self.model.mode_worktree and modified:
self.stage_hunk()
elif self.mode == self.model.mode_index:
self.unstage_hunk()
elif event.key() == QtCore.Qt.Key_S:
if self.mode == self.model.mode_worktree and modified:
self.stage_hunk_selection()
elif self.mode == self.model.mode_index:
self.unstage_hunk_selection()
def process_diff_selection(self, selected=False,
staged=True, apply_to_worktree=False,
reverse=False):
"""Implement un/staging of selected lines or hunks."""
offset, selection = self.diff_selection()
cola.notifier().broadcast(signals.apply_diff_selection,
staged,
selected,
offset,
selection,
apply_to_worktree)
def undo_hunk(self):
"""Destructively remove a hunk from a worktree file."""
if not qtutils.question(self,
'Destroy Local Changes?',
'This operation will drop '
'uncommitted changes.\n'
'Continue?',
default=False):
return
self.process_diff_selection(staged=False, apply_to_worktree=True,
reverse=True)
def undo_selection(self):
"""Destructively check out content for the selected file from $head."""
if not qtutils.question(self,
'Destroy Local Changes?',
'This operation will drop '
'uncommitted changes.\n'
'Continue?',
default=False):
return
self.process_diff_selection(staged=False, apply_to_worktree=True,
reverse=True, selected=True)
def stage(self):
"""Stage selected files, or all files if no selection exists."""
paths = cola.selection_model().unstaged
if not paths:
cola.notifier().broadcast(signals.stage_modified)
else:
cola.notifier().broadcast(signals.stage, paths)
def unstage(self):
"""Unstage selected files, or all files if no selection exists."""
paths = cola.selection_model().staged
if not paths:
cola.notifier().broadcast(signals.unstage_all)
else:
cola.notifier().broadcast(signals.unstage, paths)
def stage_hunk(self):
"""Stage a specific hunk."""
self.process_diff_selection(staged=False)
def stage_hunk_selection(self):
"""Stage selected lines."""
self.process_diff_selection(staged=False, selected=True)
def unstage_hunk(self, cached=True):
"""Unstage a hunk."""
self.process_diff_selection(staged=True)
def unstage_hunk_selection(self):
"""Unstage selected lines."""
self.process_diff_selection(staged=True, selected=True)
def diff_context_menu_event(self, event):
"""Create the context menu for the diff display."""
menu = self.diff_context_menu_setup()
textedit = self.display_text
menu.exec_(textedit.mapToGlobal(event.pos()))
def diff_context_menu_setup(self):
"""Set up the context menu for the diff display."""
menu = QtGui.QMenu(self)
staged, modified, unmerged, untracked = cola.selection()
if self.mode == self.model.mode_worktree:
if modified:
menu.addAction(self.tr('Stage &Hunk For Commit'),
self.stage_hunk)
menu.addAction(self.tr('Stage &Selected Lines'),
self.stage_hunk_selection)
menu.addSeparator()
menu.addAction(self.tr('Undo Hunk'), self.undo_hunk)
menu.addAction(self.tr('Undo Selection'), self.undo_selection)
elif self.mode == self.model.mode_index:
menu.addAction(self.tr('Unstage &Hunk From Commit'), self.unstage_hunk)
menu.addAction(self.tr('Unstage &Selected Lines'), self.unstage_hunk_selection)
elif self.mode == self.model.mode_branch:
menu.addAction(self.tr('Apply Diff to Work Tree'), self.stage_hunk)
menu.addAction(self.tr('Apply Diff Selection to Work Tree'), self.stage_hunk_selection)
elif self.mode == self.model.mode_grep:
menu.addAction(self.tr('Go Here'), self.goto_grep)
menu.addSeparator()
menu.addAction(self.tr('Copy'), self.copy_display)
return menu
def fetch(self):
"""Launch the 'fetch' remote dialog."""
remote_action(self, 'fetch')
def push(self):
"""Launch the 'push' remote dialog."""
remote_action(self, 'push')
def pull(self):
"""Launch the 'pull' remote dialog."""
remote_action(self, 'pull')
def commit(self):
"""Attempt to create a commit from the index and commit message."""
#self.reset_mode()
msg = self.model.commitmsg
if not msg:
# Describe a good commit message
error_msg = self.tr(''
'Please supply a commit message.\n\n'
'A good commit message has the following format:\n\n'
'- First line: Describe in one sentence what you did.\n'
'- Second line: Blank\n'
'- Remaining lines: Describe why this change is good.\n')
qtutils.log(1, error_msg)
cola.notifier().broadcast(signals.information,
'Missing Commit Message',
error_msg)
return
if not self.model.staged:
error_msg = self.tr(''
'No changes to commit.\n\n'
'You must stage at least 1 file before you can commit.\n')
qtutils.log(1, error_msg)
cola.notifier().broadcast(signals.information,
'No Staged Changes',
error_msg)
return
# Warn that amending published commits is generally bad
amend = self.amend_is_checked()
if (amend and self.model.is_commit_published() and
not qtutils.question(self,
'Rewrite Published Commit?',
'This commit has already been published.\n'
'You are rewriting published history.\n'
'You probably don\'t want to do this.\n\n'
'Continue?',
default=False)):
return
# Perform the commit
cola.notifier().broadcast(signals.commit, amend, msg)
def grep(self):
"""Prompt and use 'git grep' to find the content."""
# This should be a command in cola.commands.
txt, ok = qtutils.prompt('grep')
if not ok:
return
cola.notifier().broadcast(signals.grep, txt)
def goto_grep(self):
"""Called when Search -> Grep's right-click 'goto' action."""
line = self.selected_line()
filename, line_number, contents = line.split(':', 2)
filename = core.encode(filename)
cola.notifier().broadcast(signals.edit, [filename], line_number=line_number)
def open_repo(self):
"""Spawn a new cola session."""
dirname = qtutils.opendir_dialog(self,
'Open Git Repository...',
self.model.getcwd())
if not dirname:
return
cola.notifier().broadcast(signals.open_repo, dirname)
def clone_repo(self):
"""Clone a git repository."""
url, ok = qtutils.prompt('Path or URL to clone (Env. $VARS okay)')
url = os.path.expandvars(url)
if not ok or not url:
return
try:
# Pick a suitable basename by parsing the URL
newurl = url.replace('\\', '/')
default = newurl.rsplit('/', 1)[-1]
if default == '.git':
# The end of the URL is /.git, so assume it's a file path
default = os.path.basename(os.path.dirname(newurl))
if default.endswith('.git'):
# The URL points to a bare repo
default = default[:-4]
if url == '.':
# The URL is the current repo
default = os.path.basename(os.getcwd())
if not default:
raise
except:
cola.notifier().broadcast(signals.information,
'Error Cloning',
'Could not parse: "%s"' % url)
qtutils.log(1, 'Oops, could not parse git url: "%s"' % url)
return
# Prompt the user for a directory to use as the parent directory
msg = 'Select a parent directory for the new clone'
dirname = qtutils.opendir_dialog(self, msg, self.model.getcwd())
if not dirname:
return
count = 1
destdir = os.path.join(dirname, default)
olddestdir = destdir
if os.path.exists(destdir):
# An existing path can be specified
msg = ('"%s" already exists, cola will create a new directory' %
destdir)
cola.notifier().broadcast(signals.information,
'Directory Exists', msg)
# Make sure the new destdir doesn't exist
while os.path.exists(destdir):
destdir = olddestdir + str(count)
count += 1
cola.notifier().broadcast(signals.clone, url, destdir)
def cherry_pick(self):
"""Launch the 'Cherry-Pick' dialog."""
revs, summaries = self.model.log_helper(all=True)
commits = select_commits('Cherry-Pick Commit',
revs, summaries, multiselect=False)
if not commits:
return
cola.notifier().broadcast(signals.cherry_pick, commits)
def browse_commits(self):
"""Launch the 'Browse Commits' dialog."""
revs, summaries = self.model.log_helper(all=True)
select_commits('Browse Commits', revs, summaries)
def export_patches(self):
"""Run 'git format-patch' on a list of commits."""
revs, summaries = self.model.log_helper()
to_export = select_commits('Export Patches', revs, summaries)
if not to_export:
return
to_export.reverse()
revs.reverse()
cola.notifier().broadcast(signals.format_patch, to_export, revs)
def browse_current(self):
"""Launch the 'Browse Current Branch' dialog."""
branch = self.model.currentbranch
browse_git_branch(branch)
def browse_other(self):
"""Prompt for a branch and inspect content at that point in time."""
# Prompt for a branch to browse
branch = choose_from_combo('Browse Branch Files',
self.view,
self.model.all_branches())
if not branch:
return
# Launch the repobrowser
browse_git_branch(branch)
def branch_create(self):
"""Launch the 'Create Branch' dialog."""
create_new_branch()
def branch_delete(self):
"""Launch the 'Delete Branch' dialog."""
branch = choose_from_combo('Delete Branch',
self.model.local_branches)
if not branch:
return
cola.notifier().broadcast(signals.delete_branch, branch)
def checkout_branch(self):
"""Launch the 'Checkout Branch' dialog."""
branch = choose_from_combo('Checkout Branch',
self.model.local_branches)
if not branch:
return
cola.notifier().broadcast(signals.checkout_branch, branch)
def rebase(self):
"""Rebase onto a branch."""
branch = choose_from_combo('Rebase Branch',
self.model.all_branches())
if not branch:
return
#TODO cmd
status, output = self.model.git.rebase(branch,
with_stderr=True,
with_status=True)
qtutils.log(status, output)
-395
View File
@@ -1,395 +0,0 @@
from PyQt4 import QtCore
from PyQt4 import QtGui
import cola
from cola import settings
from cola import qtutils
from cola.views import log
from cola.qtutils import tr
from cola.views import status
from cola.views.standard import create_standard_widget
from cola.controllers import classic
MainWindowBase = create_standard_widget(QtGui.QMainWindow)
class MainWindow(MainWindowBase):
def __init__(self, parent=None):
MainWindowBase.__init__(self, parent)
# Default size; this is thrown out when save/restore is used
self.resize(888, 420)
self.setDockOptions(QtGui.QMainWindow.AllowNestedDocks |
QtGui.QMainWindow.AllowTabbedDocks |
QtGui.QMainWindow.AnimatedDocks)
# "Actions" widget
self.actiondockwidget = self.create_dock('Actions')
self.actiondockwidgetcontents = QtGui.QWidget()
self.actiondockwidgetcontents.setContentsMargins(2, 2, 2, 2)
self.actiondockwidgetlayout =\
QtGui.QVBoxLayout(self.actiondockwidgetcontents)
self.actiondockwidgetlayout.setSpacing(3)
self.actiondockwidgetlayout.setMargin(3)
layout = self.actiondockwidgetlayout
self.rescan_button = self.create_button('Rescan', layout)
self.stage_button = self.create_button('Stage', layout)
self.unstage_button = self.create_button('Unstage', layout)
self.fetch_button = self.create_button('Fetch', layout)
self.push_button = self.create_button('Push', layout)
self.pull_button = self.create_button('Pull', layout)
self.stash_button = self.create_button('Stash', layout)
self.alt_button = self.create_button('Exit Diff Mode', layout)
self.alt_button.hide()
self.action_spacer = QtGui.QSpacerItem(1, 1,
QtGui.QSizePolicy.Minimum,
QtGui.QSizePolicy.MinimumExpanding)
self.actiondockwidgetlayout.addItem(self.action_spacer)
self.actiondockwidget.setWidget(self.actiondockwidgetcontents)
# "Repository Status" widget
self.statusdockwidget = self.create_dock('Repository Status')
self.statusdockwidget.setWidget(status.widget())
# "Commit Message Editor" widget
self.commitdockwidget = self.create_dock('Commit Message Editor')
self.commitdockwidgetcontents = QtGui.QWidget()
self.commitdockwidgetlayout = QtGui.QVBoxLayout(self.commitdockwidgetcontents)
self.commitdockwidgetlayout.setMargin(3)
self.vboxlayout = QtGui.QVBoxLayout()
self.vboxlayout.setSpacing(0)
self.commitmsg = QtGui.QTextEdit(self.commitdockwidgetcontents)
self.commitmsg.setMinimumSize(QtCore.QSize(1, 1))
policy = QtGui.QSizePolicy(QtGui.QSizePolicy.Minimum,
QtGui.QSizePolicy.Minimum)
self.commitmsg.setSizePolicy(policy)
self.commitmsg.setLineWrapMode(QtGui.QTextEdit.NoWrap)
self.commitmsg.setAcceptRichText(False)
self.vboxlayout.addWidget(self.commitmsg)
self.hboxlayout = QtGui.QHBoxLayout()
self.hboxlayout.setSpacing(3)
# Sign off and commit buttons
self.signoff_button = self.create_button('Sign Off')
self.commit_button = self.create_button('Commit@@verb')
# Position display
self.position_label = QtGui.QLabel(self.actiondockwidgetcontents)
self.position_label.setAlignment(QtCore.Qt.AlignLeft)
# Spacer between position and amend
self.spacer = QtGui.QSpacerItem(1, 1,
QtGui.QSizePolicy.MinimumExpanding,
QtGui.QSizePolicy.Minimum)
# Amend checkbox
self.amend_checkbox = QtGui.QCheckBox(self.commitdockwidgetcontents)
self.amend_checkbox.setText(tr('Amend Last Commit'))
self.hboxlayout.addWidget(self.signoff_button)
self.hboxlayout.addWidget(self.commit_button)
self.hboxlayout.addWidget(self.position_label)
self.hboxlayout.addItem(self.spacer)
self.hboxlayout.addWidget(self.amend_checkbox)
self.vboxlayout.addLayout(self.hboxlayout)
self.commitdockwidgetlayout.addLayout(self.vboxlayout)
self.commitdockwidget.setWidget(self.commitdockwidgetcontents)
# "Command Output" widget
logwidget = qtutils.logger()
self.logdockwidget = self.create_dock('Command Output')
self.logdockwidget.setWidget(logwidget)
# "Cola Classic" widget
self.classicdockwidget = self.create_dock('Cola Classic')
self.classicdockwidget.setWidget(classic.widget())
# "Diff Viewer" widget
self.diffdockwidget = self.create_dock('Diff Viewer')
self.diffdockwidgetcontents = QtGui.QWidget()
self.diffdockwidgetlayout = QtGui.QVBoxLayout(self.diffdockwidgetcontents)
self.diffdockwidgetlayout.setMargin(3)
self.display_text = QtGui.QTextEdit(self.diffdockwidgetcontents)
self.display_text.setMinimumSize(QtCore.QSize(1, 1))
self.display_text.setLineWrapMode(QtGui.QTextEdit.NoWrap)
self.display_text.setReadOnly(True)
self.display_text.setAcceptRichText(False)
self.display_text.setCursorWidth(2)
self.display_text.setTextInteractionFlags(QtCore.Qt.LinksAccessibleByKeyboard|QtCore.Qt.LinksAccessibleByMouse|QtCore.Qt.TextBrowserInteraction|QtCore.Qt.TextSelectableByKeyboard|QtCore.Qt.TextSelectableByMouse)
self.diffdockwidgetlayout.addWidget(self.display_text)
self.diffdockwidget.setWidget(self.diffdockwidgetcontents)
# All Actions
self.menu_unstage_selected = self.create_action('Unstage From Commit')
self.menu_show_diffstat = self.create_action('Diffstat')
self.menu_stage_modified =\
self.create_action('Stage Changed Files To Commit')
self.menu_stage_untracked = self.create_action('Stage All Untracked')
self.menu_export_patches = self.create_action('Export Patches...')
self.menu_cut = self.create_action('Cut')
self.menu_copy = self.create_action('Copy')
self.menu_paste = self.create_action('Paste')
self.menu_select_all = self.create_action('Select All')
self.menu_options = self.create_action('Options')
self.menu_delete = self.create_action('Delete')
self.menu_undo = self.create_action('Undo')
self.menu_redo = self.create_action('Redo')
self.menu_rescan = self.create_action('Rescan')
self.menu_cherry_pick = self.create_action('Cherry-Pick...')
self.menu_unstage_all = self.create_action('Unstage All')
self.menu_load_commitmsg = self.create_action('Load Commit Message...')
self.menu_quit = self.create_action('Quit')
self.menu_search_revision = self.create_action('Revision ID...')
self.menu_search_path =\
self.create_action('Commits Touching Path(s)...')
self.menu_search_revision_range =\
self.create_action('Revision Range...')
self.menu_search_date_range = self.create_action('Latest Commits...')
self.menu_search_message = self.create_action('Commit Messages...')
self.menu_search_diff =\
self.create_action('Content Introduced in Commit...')
self.menu_search_author = self.create_action('Commits By Author...')
self.menu_search_committer =\
self.create_action('Commits By Committer...')
self.menu_manage_bookmarks = self.create_action('Bookmarks...')
self.menu_save_bookmark = self.create_action('Bookmark Current...')
self.menu_search_grep = self.create_action('Grep')
self.menu_merge_local = self.create_action('Merge...')
self.menu_merge_abort = self.create_action('Abort Merge...')
self.menu_open_repo = self.create_action('Open...')
self.menu_stash = self.create_action('Stash...')
self.menu_diff_branch =\
self.create_action('Apply Changes From Branch...')
self.menu_branch_compare = self.create_action('Branches...')
self.menu_clone_repo = self.create_action('Clone...')
self.menu_help_docs = self.create_action('Documentation')
self.menu_commit_compare = self.create_action('Commits...')
self.menu_visualize_current =\
self.create_action('Visualize Current Branch...')
self.menu_visualize_all =\
self.create_action('Visualize All Branches...')
self.menu_browse_commits = self.create_action('Browse Commits...')
self.menu_browse_branch =\
self.create_action('Browse Current Branch...')
self.menu_browse_other_branch =\
self.create_action('Browse Other Branch...')
self.menu_load_commitmsg_template =\
self.create_action('Get Commit Message Template')
self.menu_commit_compare_file =\
self.create_action('Commits Touching File...')
self.menu_help_about = self.create_action('About')
self.menu_branch_diff = self.create_action('SHA-1...')
self.menu_diff_expression = self.create_action('Expression...')
self.menu_create_tag = self.create_action('Create Tag...')
self.menu_create_branch = self.create_action('Create...')
self.menu_delete_branch = self.create_action('Delete...')
self.menu_checkout_branch = self.create_action('Checkout...')
self.menu_rebase_branch = self.create_action('Rebase...')
self.menu_branch_review = self.create_action('Review...')
# Create the application menu
self.menubar = QtGui.QMenuBar(self)
# File Menu
self.file_menu = self.create_menu('File', self.menubar)
self.file_menu.addAction(self.menu_open_repo)
self.file_menu.addAction(self.menu_clone_repo)
self.file_menu.addSeparator()
self.file_menu.addAction(self.menu_rescan)
self.file_menu.addSeparator()
self.file_menu.addAction(self.menu_manage_bookmarks)
self.file_menu.addAction(self.menu_save_bookmark)
self.file_menu.addSeparator()
self.file_menu.addAction(self.menu_load_commitmsg)
self.file_menu.addAction(self.menu_load_commitmsg_template)
self.file_menu.addSeparator()
self.file_menu.addAction(self.menu_quit)
# Add to menubar
self.menubar.addAction(self.file_menu.menuAction())
# Edit Menu
self.edit_menu = self.create_menu('Edit', self.menubar)
self.edit_menu.addAction(self.menu_undo)
self.edit_menu.addAction(self.menu_redo)
self.edit_menu.addSeparator()
self.edit_menu.addAction(self.menu_cut)
self.edit_menu.addAction(self.menu_copy)
self.edit_menu.addAction(self.menu_paste)
self.edit_menu.addAction(self.menu_delete)
self.edit_menu.addSeparator()
self.edit_menu.addAction(self.menu_select_all)
self.edit_menu.addSeparator()
self.edit_menu.addAction(self.menu_options)
# Add to menubar
self.menubar.addAction(self.edit_menu.menuAction())
# Commit Menu
self.commit_menu = self.create_menu('Commit', self.menubar)
self.commit_menu.addAction(self.menu_stage_modified)
self.commit_menu.addAction(self.menu_stage_untracked)
self.commit_menu.addSeparator()
self.commit_menu.addAction(self.menu_unstage_all)
self.commit_menu.addAction(self.menu_unstage_selected)
self.commit_menu.addSeparator()
self.commit_menu.addAction(self.menu_browse_commits)
# Add to menubar
self.menubar.addAction(self.commit_menu.menuAction())
# Branch Menu
self.branch_menu = self.create_menu('Branch', self.menubar)
self.branch_menu.addAction(self.menu_branch_review)
self.branch_menu.addSeparator()
self.branch_menu.addAction(self.menu_create_branch)
self.branch_menu.addAction(self.menu_checkout_branch)
self.branch_menu.addAction(self.menu_rebase_branch)
self.branch_menu.addAction(self.menu_delete_branch)
self.branch_menu.addSeparator()
self.branch_menu.addAction(self.menu_browse_branch)
self.branch_menu.addAction(self.menu_browse_other_branch)
self.branch_menu.addSeparator()
self.branch_menu.addAction(self.menu_visualize_current)
self.branch_menu.addAction(self.menu_visualize_all)
self.branch_menu.addSeparator()
self.branch_menu.addAction(self.menu_diff_branch)
# Add to menubar
self.menubar.addAction(self.branch_menu.menuAction())
# Search Menu
self.search_menu = self.create_menu('Search', self.menubar)
self.search_menu.addAction(self.menu_search_date_range)
self.search_menu.addAction(self.menu_search_grep)
self.search_menu.addSeparator()
# Search / More Menu
self.menu_search_more = self.create_menu('More...', self.search_menu)
self.menu_search_more.addAction(self.menu_search_author)
self.menu_search_more.addAction(self.menu_search_path)
self.menu_search_more.addAction(self.menu_search_message)
self.menu_search_more.addSeparator()
self.menu_search_more.addAction(self.menu_search_revision_range)
self.menu_search_more.addAction(self.menu_search_revision)
self.menu_search_more.addSeparator()
self.menu_search_more.addAction(self.menu_search_diff)
self.search_menu.addAction(self.menu_search_more.menuAction())
# Add to menubar
self.menubar.addAction(self.search_menu.menuAction())
# Actions menu
self.actions_menu = self.create_menu('Actions', self.menubar)
self.actions_menu.addAction(self.menu_merge_local)
self.actions_menu.addAction(self.menu_stash)
self.actions_menu.addSeparator()
self.actions_menu.addAction(self.menu_create_tag)
self.actions_menu.addSeparator()
self.actions_menu.addAction(self.menu_export_patches)
self.actions_menu.addAction(self.menu_cherry_pick)
self.actions_menu.addSeparator()
self.actions_menu.addAction(self.menu_merge_abort)
# Add to menubar
self.menubar.addAction(self.actions_menu.menuAction())
# Diff Menu
self.diff_menu = self.create_menu('Diff', self.menubar)
self.diff_menu.addAction(self.menu_branch_diff)
self.diff_menu.addAction(self.menu_diff_expression)
self.diff_menu.addSeparator()
self.diff_menu.addAction(self.menu_branch_compare)
self.diff_menu.addAction(self.menu_commit_compare)
self.diff_menu.addAction(self.menu_commit_compare_file)
self.diff_menu.addSeparator()
self.diff_menu.addAction(self.menu_show_diffstat)
# Add to menubar
self.menubar.addAction(self.diff_menu.menuAction())
# Tools Menu
self.tools_menu = self.create_menu('Tools', self.menubar)
self.tools_menu.addAction(self.classicdockwidget.toggleViewAction())
self.tools_menu.addAction(self.diffdockwidget.toggleViewAction())
self.tools_menu.addAction(self.actiondockwidget.toggleViewAction())
self.tools_menu.addAction(self.commitdockwidget.toggleViewAction())
self.tools_menu.addAction(self.statusdockwidget.toggleViewAction())
self.tools_menu.addAction(self.logdockwidget.toggleViewAction())
self.menubar.addAction(self.tools_menu.menuAction())
# Help Menu
self.help_menu = self.create_menu('Help', self.menubar)
self.help_menu.addAction(self.menu_help_docs)
self.help_menu.addAction(self.menu_help_about)
# Add to menubar
self.menubar.addAction(self.help_menu.menuAction())
# Set main menu
self.setMenuBar(self.menubar)
# Shortcuts
self.menu_show_diffstat.setShortcut(tr('Ctrl+D'))
self.menu_stage_modified.setShortcut(tr('Alt+A'))
self.menu_stage_untracked.setShortcut(tr('Alt+U'))
self.menu_export_patches.setShortcut(tr('Ctrl+E'))
self.menu_cut.setShortcut(tr('Ctrl+X'))
self.menu_copy.setShortcut(tr('Ctrl+C'))
self.menu_paste.setShortcut(tr('Ctrl+V'))
self.menu_select_all.setShortcut(tr('Ctrl+A'))
self.menu_options.setShortcut(tr('Ctrl+O'))
self.menu_delete.setShortcut(tr('Del'))
self.menu_undo.setShortcut(tr('Ctrl+Z'))
self.menu_redo.setShortcut(tr('Ctrl+Shift+Z'))
self.menu_rescan.setShortcut(tr('Ctrl+R'))
self.menu_cherry_pick.setShortcut(tr('Ctrl+P'))
self.menu_quit.setShortcut(tr('Ctrl+Q'))
self.menu_create_branch.setShortcut(tr('Ctrl+B'))
self.menu_checkout_branch.setShortcut(tr('Alt+B'))
self.menu_stash.setShortcut(tr('Alt+Shift+S'))
self.menu_help_docs.setShortcut(tr('F1'))
# Arrange dock widgets
top = QtCore.Qt.DockWidgetArea(4)
bottom = QtCore.Qt.DockWidgetArea(8)
self.addDockWidget(top, self.commitdockwidget)
self.addDockWidget(top, self.classicdockwidget)
self.tabifyDockWidget(self.classicdockwidget, self.statusdockwidget)
self.addDockWidget(bottom, self.actiondockwidget)
self.addDockWidget(bottom, self.logdockwidget)
self.tabifyDockWidget(self.logdockwidget, self.diffdockwidget)
# Hide the classic dock by default
self.classicdockwidget.toggleViewAction().trigger()
def create_dock(self, title):
"""Create a dock widget and set it up accordingly."""
dock = QtGui.QDockWidget(self)
dock.setWindowTitle(tr(title))
dock.setObjectName(title)
return dock
def create_menu(self, title, parent):
"""Create a menu and set its title."""
qmenu = QtGui.QMenu(parent)
qmenu.setTitle(tr(title))
return qmenu
def create_action(self, title):
"""Create an action and set its title."""
action = QtGui.QAction(self)
action.setText(tr(title))
return action
def create_button(self, text, layout=None):
"""Create a button, set its title, and add it to the parent."""
button = QtGui.QPushButton()
button.setText(tr(text))
if layout:
layout.addWidget(button)
return button
def closeEvent(self, event):
"""Save state in the settings manager."""
if cola.model().remember_gui_settings():
settings.SettingsManager.save_gui_state(self)
self.close()
-114
View File
@@ -1,114 +0,0 @@
from PyQt4 import QtGui
from PyQt4.QtCore import Qt
from PyQt4.QtCore import SIGNAL
from cola.qtutils import tr
class MergeView(QtGui.QDialog):
"""Provides a dialog for merging branches."""
def __init__(self, parent=None):
QtGui.QDialog.__init__(self, parent)
self.setWindowTitle(tr('Merge'))
self.resize(673, 339)
self.vboxlayout = QtGui.QVBoxLayout(self)
self.vboxlayout.setMargin(3)
self.merge_target_label = QtGui.QLabel(self)
self.merge_target_label.setAlignment(Qt.AlignCenter)
self.vboxlayout.addWidget(self.merge_target_label)
self.groupbox = QtGui.QGroupBox(self)
self.groupbox.setTitle(tr('Revision To Merge'))
self.gridlayout = QtGui.QGridLayout(self.groupbox)
self.gridlayout.setContentsMargins(5, 5, 5, 5)
self.revision_label = QtGui.QLabel(self.groupbox)
self.revision_label.setText(tr('Revision Expression:'))
self.gridlayout.addWidget(self.revision_label, 0, 0, 1, 1)
self.revision = QtGui.QLineEdit(self.groupbox)
self.gridlayout.addWidget(self.revision, 0, 1, 1, 2)
self.radio_local = QtGui.QRadioButton(self.groupbox)
self.radio_local.setText(tr('Local Branch'))
self.radio_local.setChecked(True)
self.gridlayout.addWidget(self.radio_local, 1, 0, 1, 1)
self.radio_remote = QtGui.QRadioButton(self.groupbox)
self.radio_remote.setText(tr('Tracking Branch'))
self.gridlayout.addWidget(self.radio_remote, 1, 1, 1, 1)
self.radio_tag = QtGui.QRadioButton(self.groupbox)
self.radio_tag.setText(tr('Tag'))
self.gridlayout.addWidget(self.radio_tag, 1, 2, 1, 1)
self.revisions = QtGui.QListWidget(self.groupbox)
self.revisions.setAlternatingRowColors(True)
self.gridlayout.addWidget(self.revisions, 2, 0, 1, 3)
self.vboxlayout.addWidget(self.groupbox)
self.hboxlayout = QtGui.QHBoxLayout()
self.button_viz = QtGui.QPushButton(self)
self.button_viz.setText(tr('Visualize'))
self.hboxlayout.addWidget(self.button_viz)
self.spacer = QtGui.QSpacerItem(231, 24, QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Minimum)
self.hboxlayout.addItem(self.spacer)
self.checkbox_squash = QtGui.QCheckBox(self)
self.checkbox_squash.setText(tr('Squash'))
self.hboxlayout.addWidget(self.checkbox_squash)
self.checkbox_commit = QtGui.QCheckBox(self)
self.checkbox_commit.setText(tr('Commit'))
self.checkbox_commit.setChecked(True)
self.checkbox_commit_state = True
self.hboxlayout.addWidget(self.checkbox_commit)
self.button_cancel = QtGui.QPushButton(self)
self.button_cancel.setText(tr('Cancel'))
self.hboxlayout.addWidget(self.button_cancel)
self.button_merge = QtGui.QPushButton(self)
self.button_merge.setText(tr('Merge'))
self.hboxlayout.addWidget(self.button_merge)
self.vboxlayout.addLayout(self.hboxlayout)
self.connect(self.button_cancel, SIGNAL('clicked()'),
self.reject)
self.connect(self.checkbox_squash, SIGNAL('clicked()'),
self.toggle_squash)
self.set_branch('master')
self.revision.setFocus()
def set_branch(self, branch):
"""Set the branch name for the window title and label."""
txt = unicode(tr('Merge into %s')) % branch
self.merge_target_label.setText(txt)
def toggle_squash(self):
"""Toggles the commit checkbox based on the squash checkbox."""
if self.checkbox_squash.isChecked():
self.checkbox_commit_state =\
self.checkbox_commit.checkState()
self.checkbox_commit.setCheckState(Qt.Unchecked)
self.checkbox_commit.setDisabled(True)
else:
self.checkbox_commit.setDisabled(False)
oldstate = self.checkbox_commit_state
self.checkbox_commit.setCheckState(oldstate)
if __name__ == "__main__":
import sys
app = QtGui.QApplication(sys.argv)
merge = MergeView()
merge.raise_()
merge.show()
sys.exit(app.exec_())
-246
View File
@@ -1,246 +0,0 @@
from PyQt4 import QtCore
from PyQt4 import QtGui
from PyQt4.QtCore import SIGNAL
from cola.views import standard
class OptionsView(standard.StandardDialog):
def __init__(self, parent=None):
standard.StandardDialog.__init__(self, parent=parent)
#self.setWindowModality(QtCore.Qt.ApplicationModal)
self.setWindowTitle(self.tr('Options'))
self.resize(608, 425)
self._main_layt = QtGui.QVBoxLayout(self)
self._main_grid = QtGui.QGridLayout()
self._diff_font_label = QtGui.QLabel(self)
self._diff_font_label.setText(self.tr('Diff/Console Font'))
self._main_grid.addWidget(self._diff_font_label, 0, 0, 1, 1)
# Exposed
self.global_cola_fontdiff_size = QtGui.QSpinBox(self)
self.global_cola_fontdiff_size.setMinimum(8)
self.global_cola_fontdiff_size.setProperty('value', QtCore.QVariant(12))
self._main_grid.addWidget(self.global_cola_fontdiff_size, 0, 1, 1, 1)
# Exposed
self.global_cola_fontdiff = QtGui.QFontComboBox(self)
self.global_cola_fontdiff.setFontFilters(QtGui.QFontComboBox.MonospacedFonts)
self._main_grid.addWidget(self.global_cola_fontdiff, 0, 2, 1, 2)
self._tab_width_label = QtGui.QLabel(self)
self._tab_width_label.setText(self.tr('Tab Width'))
self._main_grid.addWidget(self._tab_width_label, 1, 0, 1, 1)
# Exposed
self.global_cola_tabwidth = QtGui.QSpinBox(self)
self.global_cola_tabwidth.setWrapping(True)
self.global_cola_tabwidth.setMaximum(42)
self.global_cola_tabwidth.setProperty('value', QtCore.QVariant(8))
self._main_grid.addWidget(self.global_cola_tabwidth, 1, 1, 1, 1)
# Exposed
self.global_cola_savewindowsettings = QtGui.QCheckBox(self)
self.global_cola_savewindowsettings.setText(self.tr('Save GUI Settings'))
self._main_grid.addWidget(self.global_cola_savewindowsettings, 1, 2, 1, 1)
# Exposed
self.global_merge_keepbackup = QtGui.QCheckBox(self)
self.global_merge_keepbackup.setText(self.tr('Save *.orig Merge Backups'))
self._main_grid.addWidget(self.global_merge_keepbackup, 1, 3, 1, 1)
self._editor_label = QtGui.QLabel(self)
self._editor_label.setText(self.tr('Editor'))
self._main_grid.addWidget(self._editor_label, 2, 0, 1, 1)
# Exposed
self.global_gui_editor = QtGui.QLineEdit(self)
self._main_grid.addWidget(self.global_gui_editor, 2, 1, 1, 1)
self._difftool_label = QtGui.QLabel(self)
self._difftool_label.setText(self.tr('Diff Tool'))
self._main_grid.addWidget(self._difftool_label, 2, 2, 1, 1)
# Exposed
self.global_diff_tool = QtGui.QLineEdit(self)
self._main_grid.addWidget(self.global_diff_tool, 2, 3, 1, 1)
self._historybrowser_label = QtGui.QLabel(self)
self._historybrowser_label.setText(self.tr('History Browser'))
self._main_grid.addWidget(self._historybrowser_label, 3, 0, 1, 1)
# Exposed
self.global_gui_historybrowser = QtGui.QLineEdit(self)
self._main_grid.addWidget(self.global_gui_historybrowser,
3, 1, 1, 1)
self._mergetool_label = QtGui.QLabel(self)
self._mergetool_label.setText(self.tr('Merge Tool'))
self._main_grid.addWidget(self._mergetool_label, 3, 2, 1, 1)
# Exposed
self.global_merge_tool = QtGui.QLineEdit(self)
self._main_grid.addWidget(self.global_merge_tool, 3, 3, 1, 1)
self._main_layt.addLayout(self._main_grid)
self._repo_horiz_layt = QtGui.QHBoxLayout()
self._repo_horiz_layt.setSpacing(3)
# Exposed
self.local_groupbox = QtGui.QGroupBox(self)
self.local_groupbox.setTitle(self.tr('Local Repository'))
self._local_repo_layt = QtGui.QGridLayout(self.local_groupbox)
self._local_user_name_label = QtGui.QLabel(self.local_groupbox)
self._local_user_name_label.setText(self.tr('User Name'))
self._local_repo_layt.addWidget(self._local_user_name_label,
0, 0, 1, 1)
# Exposed
self.local_user_name = QtGui.QLineEdit(self.local_groupbox)
self._local_repo_layt.addWidget(self.local_user_name,
0, 1, 1, 2)
self._local_user_email_label = QtGui.QLabel(self.local_groupbox)
self._local_user_email_label.setText(self.tr('Email Address'))
self._local_repo_layt.addWidget(self._local_user_email_label,
1, 0, 1, 1)
# Exposed
self.local_user_email = QtGui.QLineEdit(self.local_groupbox)
self._local_repo_layt.addWidget(self.local_user_email,
1, 1, 1, 2)
self._local_merge_verb_label = QtGui.QLabel(self.local_groupbox)
self._local_merge_verb_label.setText(self.tr('Merge Verbosity'))
self._local_repo_layt.addWidget(self._local_merge_verb_label,
2, 0, 1, 2)
# Exposed
self.local_merge_verbosity = QtGui.QSpinBox(self.local_groupbox)
self.local_merge_verbosity.setMinimum(0)
self.local_merge_verbosity.setMaximum(5)
self.local_merge_verbosity.setProperty('value', QtCore.QVariant(5))
self._local_repo_layt.addWidget(self.local_merge_verbosity,
2, 2, 1, 1)
self._local_diffctxt_label = QtGui.QLabel(self.local_groupbox)
self._local_diffctxt_label.setText(self.tr('Number of Diff Context Lines'))
self._local_repo_layt.addWidget(self._local_diffctxt_label,
3, 0, 1, 2)
# Exposed
self.local_gui_diffcontext = QtGui.QSpinBox(self.local_groupbox)
self.local_gui_diffcontext.setMinimum(2)
self.local_gui_diffcontext.setProperty('value', QtCore.QVariant(5))
self._local_repo_layt.addWidget(self.local_gui_diffcontext,
3, 2, 1, 1)
# Exposed
self.local_merge_summary = QtGui.QCheckBox(self.local_groupbox)
self.local_merge_summary.setText(self.tr('Summarize Merge Commits'))
self._local_repo_layt.addWidget(self.local_merge_summary,
4, 0, 1, 2)
# Exposed
self.local_merge_diffstat = QtGui.QCheckBox(self.local_groupbox)
self.local_merge_diffstat.setText(self.tr('Show Diffstat After Merge'))
self._local_repo_layt.addWidget(self.local_merge_diffstat,
5, 0, 1, 2)
self._repo_horiz_layt.addWidget(self.local_groupbox)
self._global_repo_grp = QtGui.QGroupBox(self)
self._global_repo_grp.setTitle(self.tr('Global (All Repositories)'))
self._global_repo_lyt = QtGui.QGridLayout(self._global_repo_grp)
self._global_user_name_label = QtGui.QLabel(self._global_repo_grp)
self._global_user_name_label.setText(self.tr('User Name'))
self._global_repo_lyt.addWidget(self._global_user_name_label,
0, 0, 1, 1)
# Exposed
self.global_user_name = QtGui.QLineEdit(self._global_repo_grp)
self._global_repo_lyt.addWidget(self.global_user_name,
0, 1, 1, 2)
self._global_user_email_label = QtGui.QLabel(self._global_repo_grp)
self._global_user_email_label.setText(self.tr('Email Address'))
self._global_repo_lyt.addWidget(self._global_user_email_label,
1, 0, 1, 1)
# Exposed
self.global_user_email = QtGui.QLineEdit(self._global_repo_grp)
self._global_repo_lyt.addWidget(self.global_user_email,
1, 1, 1, 2)
self._global_merge_verb_label = QtGui.QLabel(self._global_repo_grp)
self._global_merge_verb_label.setText(self.tr('Merge Verbosity'))
self._global_repo_lyt.addWidget(self._global_merge_verb_label,
2, 0, 1, 2)
# Exposed
self.global_merge_verbosity = QtGui.QSpinBox(self._global_repo_grp)
self.global_merge_verbosity.setMinimum(0)
self.global_merge_verbosity.setMaximum(5)
self.global_merge_verbosity.setProperty('value', QtCore.QVariant(5))
self._global_repo_lyt.addWidget(self.global_merge_verbosity,
2, 2, 1, 1)
self._global_diffctxt_label = QtGui.QLabel(self._global_repo_grp)
self._global_diffctxt_label.setText(
self.tr('Number of Diff Context Lines'))
self._global_repo_lyt.addWidget(self._global_diffctxt_label,
3, 0, 1, 2)
# Exposed
self.global_gui_diffcontext = QtGui.QSpinBox(self._global_repo_grp)
self.global_gui_diffcontext.setMinimum(2)
self.global_gui_diffcontext.setProperty('value', QtCore.QVariant(5))
self._global_repo_lyt.addWidget(self.global_gui_diffcontext, 3, 2, 1, 1)
# Exposed
self.global_merge_summary = QtGui.QCheckBox(self._global_repo_grp)
self.global_merge_summary.setText(self.tr('Summarize Merge Commits'))
self._global_repo_lyt.addWidget(self.global_merge_summary, 4, 0, 1, 2)
# Exposed
self.global_merge_diffstat = QtGui.QCheckBox(self._global_repo_grp)
self.global_merge_diffstat.setText(self.tr('Show Diffstat After Merge'))
self._global_repo_lyt.addWidget(self.global_merge_diffstat, 5, 0, 1, 2)
self._repo_horiz_layt.addWidget(self._global_repo_grp)
self._main_layt.addLayout(self._repo_horiz_layt)
# Save/Cancel buttons
self._button_layt = QtGui.QHBoxLayout()
self._button_spacer = QtGui.QSpacerItem(1, 1,
QtGui.QSizePolicy.Expanding,
QtGui.QSizePolicy.Minimum)
self._button_layt.addItem(self._button_spacer)
# Exposed
self.save_button = QtGui.QPushButton(self)
self.save_button.setText(self.tr('Save'))
self.save_button.setDefault(True)
self._button_layt.addWidget(self.save_button)
# Exposed
self.cancel_button = QtGui.QPushButton(self)
self.cancel_button.setText(self.tr('Cancel'))
self._button_layt.addWidget(self.cancel_button)
self._main_layt.addLayout(self._button_layt)
self.connect(self.cancel_button, SIGNAL('released()'), self.reject)
if __name__ == "__main__":
import sys
app = QtGui.QApplication(sys.argv)
options = OptionsView()
options.show()
sys.exit(app.exec_())
+55 -26
View File
@@ -2,18 +2,20 @@ from PyQt4 import QtCore
from PyQt4 import QtGui
from PyQt4.QtCore import SIGNAL
from cola import qtutils
from cola.views import standard
class RemoteView(standard.StandardDialog):
class RemoteView(standard.Dialog):
"""A dialog for choosing branches."""
def __init__(self, parent, action):
"""Customizes the dialog based on the remote action
"""
standard.StandardDialog.__init__(self, parent=parent)
standard.Dialog.__init__(self, parent=parent)
self.setWindowModality(QtCore.Qt.WindowModal)
self.resize(550, 512)
self.resize(666, 420)
self._main_vbox_layt = QtGui.QVBoxLayout(self)
# Local branch section
@@ -21,40 +23,27 @@ class RemoteView(standard.StandardDialog):
# Exposed
self.local_label = QtGui.QLabel(self)
self.local_label.setText(self.tr('Local Branches'))
self._local_branch_hbox_layt.addWidget(self.local_label)
# Exposed
self.local_branch = QtGui.QLineEdit(self)
self._local_branch_hbox_layt.addWidget(self.local_branch)
self._main_vbox_layt.addLayout(self._local_branch_hbox_layt)
# Exposed
self.local_branches = QtGui.QListWidget(self)
self._main_vbox_layt.addWidget(self.local_branches)
# Remote branch section
self._remote_branch_hbox_layt = QtGui.QHBoxLayout()
self._remote_label = QtGui.QLabel(self)
self._remote_label.setText(self.tr('Remote'))
self._remote_branch_hbox_layt.addWidget(self._remote_label)
# Exposed
self.remotename = QtGui.QLineEdit(self)
self._remote_branch_hbox_layt.addWidget(self.remotename)
self._main_vbox_layt.addLayout(self._remote_branch_hbox_layt)
# Exposed
self.remotes = QtGui.QListWidget(self)
self._main_vbox_layt.addWidget(self.remotes)
self._remote_branches_hbox_layt = QtGui.QHBoxLayout()
# Exposed
self.remote_label = QtGui.QLabel(self)
self.remote_label.setText(self.tr('Remote Branch'))
self._remote_branches_hbox_layt.addWidget(self.remote_label)
# Exposed
self.remote_branch = QtGui.QLineEdit(self)
self._remote_branches_hbox_layt.addWidget(self.remote_branch)
self._main_vbox_layt.addLayout(self._remote_branches_hbox_layt)
self.remote_branches = QtGui.QListWidget(self)
self._main_vbox_layt.addWidget(self.remote_branches)
self._options_hbox_layt = QtGui.QHBoxLayout()
# Exposed
@@ -62,14 +51,13 @@ class RemoteView(standard.StandardDialog):
self.ffwd_only_checkbox.setText(self.tr('Fast Forward Only'))
self.ffwd_only_checkbox.setChecked(True)
self.ffwd_only_checkbox.setObjectName("ffwd_only_checkbox")
self._options_hbox_layt.addWidget(self.ffwd_only_checkbox)
# Exposed
self.tags_checkbox = QtGui.QCheckBox(self)
self.tags_checkbox.setText(self.tr('Include tags'))
self._options_hbox_layt.addWidget(self.tags_checkbox)
self.rebase_checkbox = QtGui.QCheckBox(self)
self.rebase_checkbox.setText(self.tr('Rebase'))
self._options_hbox_layt.addWidget(self.rebase_checkbox)
self._options_spacer = QtGui.QSpacerItem(1, 1,
QtGui.QSizePolicy.Expanding,
@@ -78,25 +66,66 @@ class RemoteView(standard.StandardDialog):
# Exposed
self.action_button = QtGui.QPushButton(self)
self.action_button.setText(self.tr('Push'))
self._options_hbox_layt.addWidget(self.action_button)
self.action_button.setIcon(qtutils.ok_icon())
# Exposed
self.cancel_button = QtGui.QPushButton(self)
self.cancel_button.setText(self.tr('Cancel'))
self._options_hbox_layt.addWidget(self.cancel_button)
self._main_vbox_layt.addLayout(self._options_hbox_layt)
self.close_button = QtGui.QPushButton(self)
self.close_button.setText(self.tr('Close'))
self.close_button.setIcon(qtutils.close_icon())
# connections
self.connect(self.close_button, SIGNAL('released()'), self.reject)
if action:
self.action_button.setText(action.title())
self.setWindowTitle(action.title())
if action == 'pull':
self.tags_checkbox.hide()
self.ffwd_only_checkbox.hide()
self.local_label.hide()
self.local_branch.hide()
self.local_branches.hide()
if action != 'pull':
self.remote_branch.setFocus()
else:
self.rebase_checkbox.hide()
self.connect(self.cancel_button, SIGNAL('released()'), self.reject)
self.remotename.setFocus()
self.layout_remotes()
if action == 'push':
self.layout_local_branches()
self.layout_remote_branches()
else: # fetch and pull
self.layout_remote_branches()
self.layout_local_branches()
self.layout_options()
def layout_options(self):
self._options_hbox_layt.addWidget(self.ffwd_only_checkbox)
self._options_hbox_layt.addWidget(self.tags_checkbox)
self._options_hbox_layt.addWidget(self.rebase_checkbox)
self._options_hbox_layt.addWidget(self.action_button)
self._options_hbox_layt.addWidget(self.close_button)
self._main_vbox_layt.addLayout(self._options_hbox_layt)
def layout_local_branches(self):
self._local_branch_hbox_layt.addWidget(self.local_label)
self._local_branch_hbox_layt.addWidget(self.local_branch)
self._main_vbox_layt.addLayout(self._local_branch_hbox_layt)
self._main_vbox_layt.addWidget(self.local_branches)
def layout_remotes(self):
self._remote_branch_hbox_layt.addWidget(self._remote_label)
self._remote_branch_hbox_layt.addWidget(self.remotename)
self._main_vbox_layt.addLayout(self._remote_branch_hbox_layt)
self._main_vbox_layt.addWidget(self.remotes)
def layout_remote_branches(self):
self._remote_branches_hbox_layt.addWidget(self.remote_label)
self._remote_branches_hbox_layt.addWidget(self.remote_branch)
self._main_vbox_layt.addLayout(self._remote_branches_hbox_layt)
self._main_vbox_layt.addWidget(self.remote_branches)
def select_first_remote(self):
"""Selects the first remote in the list view"""
+7 -7
View File
@@ -4,14 +4,14 @@ from PyQt4 import QtGui
from PyQt4 import QtCore
from PyQt4.QtCore import SIGNAL
from cola import qtutils
from cola.prefs import diff_font
from cola.qt import DiffSyntaxHighlighter
from cola.views import standard
from cola.views import syntax
class SearchView(standard.StandardDialog):
class SearchView(standard.Dialog):
def __init__(self, parent=None):
standard.StandardDialog.__init__(self, parent=parent)
standard.Dialog.__init__(self, parent=parent)
self.setWindowTitle(self.tr('Search'))
if self.parent():
@@ -105,9 +105,9 @@ class SearchView(standard.StandardDialog):
self.commit_text.setTabChangesFocus(True)
self.commit_text.setReadOnly(True)
self.commit_text.setAcceptRichText(False)
qtutils.set_diff_font(self.commit_text)
self._syntax = syntax.DiffSyntaxHighlighter(self.commit_text.document(),
whitespace=False)
self.commit_text.setFont(diff_font())
self._syntax = DiffSyntaxHighlighter(self.commit_text.document(),
whitespace=False)
self._main_vbox_layt.addWidget(self.splitter)
self._button_grid_layt = QtGui.QGridLayout()
+3 -3
View File
@@ -2,8 +2,8 @@ from PyQt4 import QtGui
from PyQt4 import QtCore
from PyQt4.QtCore import SIGNAL
from cola import qtutils
from cola.views.syntax import DiffSyntaxHighlighter
from cola.prefs import diff_font
from cola.qt import DiffSyntaxHighlighter
class SelectCommitsView(QtGui.QDialog):
def __init__(self,
@@ -80,4 +80,4 @@ class SelectCommitsView(QtGui.QDialog):
# Set the console font
if syntax:
qtutils.set_diff_font(self.commit_text)
self.commit_text.setFont(diff_font())
+8 -8
View File
@@ -1,7 +1,6 @@
from PyQt4 import QtCore
from PyQt4 import QtGui
from cola.views import syntax
from cola import qt
def create_standard_widget(qtclass):
@@ -12,7 +11,7 @@ def create_standard_widget(qtclass):
def __init__(self, parent=None):
self._qtclass = qtclass
self._qtclass.__init__(self, parent)
syntax.set_theme_properties(self)
qt.set_theme_properties(self)
def show(self):
"""Automatically centers and raises dialogs"""
@@ -26,14 +25,14 @@ def create_standard_widget(qtclass):
self.move(x, y)
# Call the base Qt show()
self._qtclass.show(self)
self.raise_()
self._qtclass.show(self)
def name(self):
"""Returns the name of the view class"""
return self.__class__.__name__.lower()
def import_state(self, settings):
def apply_state(self, settings):
"""Imports data for view save/restore"""
if 'width' in settings and 'height' in settings:
w = settings.get('width')
@@ -61,7 +60,7 @@ def create_standard_widget(qtclass):
def style_properties(self):
# user-definable color properties
props = {}
for name in syntax.default_colors:
for name in qt.default_colors:
props[name] = getattr(self, '_'+name)
return props
@@ -70,9 +69,10 @@ def create_standard_widget(qtclass):
self.syntax.set_colors(self.style_properties())
self.syntax.reset()
syntax.install_style_properties(StandardWidget)
qt.install_style_properties(StandardWidget)
return StandardWidget
# The base class for all cola QDialogs.
StandardDialog = create_standard_widget(QtGui.QDialog)
Dialog = create_standard_widget(QtGui.QDialog)
MainWindow = create_standard_widget(QtGui.QMainWindow)

Some files were not shown because too many files have changed in this diff Show More