From 992e810a478dfe9670a4161830883d21035cb1d9 Mon Sep 17 00:00:00 2001 From: David Aguilar Date: Sat, 29 Dec 2007 18:13:42 -0800 Subject: [PATCH] Refactored several methods out of the main controller. Added a utilcontroller module to own simple dialogs such as choose_branch Moved select_commits into utilcontroller Moved browse_git_branch into the repobrowsercontroller Removed lots of view classes from the controller module Sped up the push dialog's init time by asking the config system for the remote url Speed up cloning by passing constructor args to model.clone() Signed-off by: David Aguilar --- ugitlibs/controllers.py | 116 +++++++------------------------------ ugitlibs/createbranchcontroller.py | 10 ++++ ugitlibs/git.py | 44 ++------------ ugitlibs/model.py | 4 +- ugitlibs/models.py | 28 +++++---- ugitlibs/pushcontroller.py | 20 +++++-- ugitlibs/repobrowsercontroller.py | 13 +++++ ugitlibs/utilcontroller.py | 69 ++++++++++++++++++++++ ugitlibs/utils.py | 42 ++++++++++++-- ugitlibs/views.py | 49 ++++++++-------- ui/Window.ui | 2 +- 11 files changed, 213 insertions(+), 184 deletions(-) create mode 100644 ugitlibs/utilcontroller.py diff --git a/ugitlibs/controllers.py b/ugitlibs/controllers.py index bdc958b..18ffa03 100644 --- a/ugitlibs/controllers.py +++ b/ugitlibs/controllers.py @@ -1,22 +1,20 @@ #!/usr/bin/env python import os -import commands + from PyQt4 import QtGui -from PyQt4 import QtCore from PyQt4.QtGui import QDialog from PyQt4.QtGui import QMessageBox from PyQt4.QtGui import QMenu -from qobserver import QObserver + import utils import qtutils import defaults -from views import PushDialog -from views import BranchDialog -from views import CreateBranchDialog -from views import CommitBrowser -from repobrowsercontroller import RepoBrowserController -from createbranchcontroller import CreateBranchController -from pushcontroller import PushController +from qobserver import QObserver +from repobrowsercontroller import browse_git_branch +from createbranchcontroller import create_new_branch +from pushcontroller import push_branches +from utilcontroller import choose_branch +from utilcontroller import select_commits class Controller(QObserver): '''The controller is a mediator between the model and view. @@ -180,31 +178,29 @@ class Controller(QObserver): # Qt callbacks def branch_create(self): - view = CreateBranchDialog(self.view) - controller = CreateBranchController(self.model, view) - view.show() - result = view.exec_() - if result == QDialog.Accepted: self.rescan() + if create_new_branch(self.view, self.model): + self.rescan() def branch_delete(self): - branch = BranchDialog.choose('Delete Branch', + branch = choose_branch('Delete Branch', self.view, self.model.get_local_branches()) if not branch: return self.show_output(self.model.delete_branch(branch)) def browse_current(self): - self.__browse_branch(self.model.get_branch()) + branch = self.model.get_branch() + browse_git_branch(self.model, self.view, branch) def browse_other(self): # Prompt for a branch to browse - branch = BranchDialog.choose('Browse Branch Files', + branch = choose_branch('Browse Branch Files', self.view, self.model.get_all_branches()) if not branch: return # Launch the repobrowser - self.__browse_branch(branch) + browse_git_branch(self.model, self.view, branch) def checkout_branch(self): - branch = BranchDialog.choose('Checkout Branch', + branch = choose_branch('Checkout Branch', self.view, self.model.get_local_branches()) if not branch: return self.show_output(self.model.checkout(branch)) @@ -212,7 +208,7 @@ class Controller(QObserver): def cherry_pick(self): '''Starts a cherry-picking session.''' (revs, summaries) = self.model.log(all=True) - selection, idxs = self.__select_commits(revs, summaries) + selection, idxs = self.select_commits_gui(revs, summaries) if not selection: return output = self.model.cherry_pick(selection) self.show_output(self.tr(output)) @@ -250,32 +246,6 @@ class Controller(QObserver): self.model.set_commitmsg('') self.show_output(output) - def commit_sha1_selected(self, browser, revs): - '''This callback is called when a commit browser's - item is selected. This callback puts the current - revision sha1 into the commitText field. - This callback also puts shows the commit in the - browser's commit textedit and copies it into - the global clipboard/selection.''' - current = browser.commitList.currentRow() - item = browser.commitList.item(current) - if not item.isSelected(): - browser.commitText.setText('') - browser.revisionLine.setText('') - return - - # Get the sha1 and put it in the revision line - sha1 = revs[current] - browser.revisionLine.setText(sha1) - browser.revisionLine.selectAll() - - # Lookup the sha1's commit - commit_diff = self.model.diff(commit=sha1,cached=False) - browser.commitText.setText(commit_diff) - - # Copy the sha1 into the clipboard - qtutils.set_clipboard(sha1) - def view_diff(self, staged=True): self.__staged_diff_in_view = staged if self.__staged_diff_in_view: @@ -305,7 +275,7 @@ class Controller(QObserver): patches.''' (revs, summaries) = self.model.log() - selection, idxs = self.__select_commits(revs, summaries) + selection, idxs = self.select_commits_gui(revs, summaries) if not selection: return # now get the selected indices to determine whether @@ -337,7 +307,7 @@ class Controller(QObserver): if slushy: self.model.set_commitmsg(slushy) def rebase(self): - branch = BranchDialog.choose('Rebase Branch', + branch = choose_branch('Rebase Branch', self.view, self.model.get_local_branches()) if not branch: return self.show_output(self.model.rebase(branch)) @@ -372,11 +342,7 @@ class Controller(QObserver): self.model.set_squash_msg() def push(self): - model = self.model.clone() - view = PushDialog(self.view) - controller = PushController(model,view) - view.show() - view.exec_() + push_branches(self.model, self.view) def show_diffstat(self): '''Show the diffstat from the latest commit.''' @@ -498,17 +464,6 @@ class Controller(QObserver): self.rescan() return output - def __browse_branch(self, branch): - if not branch: return - # Clone the model to allow opening multiple browsers - # with different sets of data - model = self.model.clone() - model.set_branch(branch) - view = CommitBrowser() - controller = RepoBrowserController(model, view) - view.show() - view.exec_() - def __menu_about_to_show(self): unstaged_item = qtutils.get_selected_item( @@ -565,35 +520,8 @@ class Controller(QObserver): self.connect(self.__menu, 'aboutToShow()', self.__menu_about_to_show) - def __select_commits(self, revs, summaries): - '''Use the CommitBrowser to select commits from a list.''' - if not summaries: - msg = self.tr('No commits exist in this branch.') - self.show_output(msg) - return([],[]) - - browser = CommitBrowser(self.view) - self.connect(browser.commitList, - 'itemSelectionChanged()', - lambda: self.commit_sha1_selected( - browser, revs) ) - - qtutils.set_items(browser.commitList, summaries) - - browser.show() - result = browser.exec_() - if result != QDialog.Accepted: - return([],[]) - - list_widget = browser.commitList - selection = qtutils.get_selection_list(list_widget, revs) - if not selection: return([],[]) - - # also return the selected index numbers - index_nums = range(len(revs)) - idxs = qtutils.get_selection_list(list_widget, index_nums) - - return(selection, idxs) + def select_commits_gui(self, revs, summaries): + return select_commits(self.model, self.view, revs, summaries) def __start_inotify_thread(self): # Do we have inotify? If not, return. diff --git a/ugitlibs/createbranchcontroller.py b/ugitlibs/createbranchcontroller.py index 7424bdb..a8059e6 100644 --- a/ugitlibs/createbranchcontroller.py +++ b/ugitlibs/createbranchcontroller.py @@ -1,8 +1,18 @@ #!/usr/bin/env python import os +from PyQt4.QtGui import QDialog + import utils import qtutils from qobserver import QObserver +from views import CreateBranchDialog + +def create_new_branch(parent,model): + model = model.clone(init=False) + view = CreateBranchDialog(parent) + ctl = CreateBranchController(model,view) + view.show() + return view.exec_() == QDialog.Accepted class CreateBranchController(QObserver): def __init__(self, model, view): diff --git a/ugitlibs/git.py b/ugitlibs/git.py index 6c22198..0b8d571 100644 --- a/ugitlibs/git.py +++ b/ugitlibs/git.py @@ -5,10 +5,6 @@ import types import utils from cStringIO import StringIO -from PyQt4.QtCore import QProcess -from PyQt4.QtCore import QObject -import PyQt4.QtGui - # A regex for matching the output of git(log|rev-list) --pretty=oneline REV_LIST_REGEX = re.compile('([0-9a-f]+)\W(.*)') @@ -16,34 +12,7 @@ def quote(argv): return ' '.join([ utils.shell_quote(arg) for arg in argv ]) def git(*args,**kwargs): - return run_cmd('git', *args, **kwargs) - -def run_cmd(cmd, *args, **kwargs): - # Handle cmd as either a string or an argv list - if type(cmd) is str: - cmd = cmd.split(' ') - cmd += list(args) - else: - cmd = list(cmd + list(args)) - - child = QProcess() - child.setProcessChannelMode(QProcess.MergedChannels); - child.start(cmd[0], cmd[1:]) - - if not child.waitForStarted(): raise Exception("failed to start child") - if not child.waitForFinished(): raise Exception("failed to start child") - - output = str(child.readAll()) - - # Allow run_cmd(argv, raw=True) for when we - # want the full, raw output(e.g. git cat-file) - if 'raw' in kwargs: - return output - else: - if 'with_status' in kwargs: - return child.exitCode(), output.rstrip() - else: - return output.rstrip() + return utils.run_cmd('git', *args, **kwargs) def add(to_add): '''Invokes 'git add' to index the filenames in to_add.''' @@ -296,11 +265,8 @@ def remote(*args): argv = ['remote'] + list(args) return git(*argv).splitlines() -def remote_show(name): - return [ line.strip() for line in remote('show', name) ] - def remote_url(name): - return utils.grep('^URL:\s+(.*)', remote_show(name)) + return config('remote.%s.url' % name) def reset(to_unstage): '''Use 'git reset' to unstage files from the index.''' @@ -324,10 +290,8 @@ def rev_list_range(start, end): revs.append((rev_id, summary,) ) return revs -def show(sha1, color=False): - cmd = 'git show ' - if color: cmd += '--color ' - return run_cmd(cmd + sha1) +def show(sha1): + return git('show',sha1) def show_cdup(): '''Returns a relative path to the git project root.''' diff --git a/ugitlibs/model.py b/ugitlibs/model.py index 99e13d6..da75c3e 100644 --- a/ugitlibs/model.py +++ b/ugitlibs/model.py @@ -51,8 +51,8 @@ class Model(Observable): def create(self,**kwargs): return self.from_dict(kwargs) - def clone(self): - return self.__class__().from_dict(self.to_dict()) + def clone(self, *args, **kwargs): + return self.__class__(*args, **kwargs).from_dict(self.to_dict()) def set_list_attrs(self, list_attrs): self.__list_attrs.update(list_attrs) diff --git a/ugitlibs/models.py b/ugitlibs/models.py index c163492..39536ee 100644 --- a/ugitlibs/models.py +++ b/ugitlibs/models.py @@ -6,13 +6,21 @@ import utils import model class Model(model.Model): - def __init__(self): + def __init__(self, init=True): model.Model.__init__(self) + # These methods are best left implemented in git.py + for attr in ('add', 'add_or_remove', 'cat_file', 'checkout', + 'create_branch', 'cherry_pick', 'commit', 'diff', + 'diff_stat', 'format_patch', 'push', 'show','log', + 'rebase', 'remote_url', 'rev_list_range'): + setattr(self, attr, getattr(git,attr)) + # chdir to the root of the git tree. This is critical # to being able to properly use the git porcelain. cdup = git.show_cdup() if cdup: os.chdir(cdup) + if not init: return self.create( ##################################################### @@ -43,8 +51,10 @@ class Model(model.Model): tags = git.tag(), ##################################################### - # Used by the repo browser + # Used by the commit/repo browser directory = '', + revisions = [], + summaries = [], # These are parallel lists types = [], @@ -62,13 +72,6 @@ class Model(model.Model): subtree_names = [], ) - # These methods are best left implemented in git.py - for attr in ('add', 'add_or_remove', 'cat_file', 'checkout', - 'create_branch', 'cherry_pick', 'commit', 'diff', - 'diff_stat', 'format_patch', 'push', 'show','log', - 'rebase', 'remote_url', 'rev_list_range'): - setattr(self, attr, getattr(git,attr)) - def init_browser_data(self): '''This scans over self.(names, sha1s, types) to generate directories, directory_entries, and subtree_*''' @@ -241,6 +244,9 @@ class Model(model.Model): def delete_branch(self, branch): return git.branch(name=branch, delete=True) + def get_revision_sha1(self, idx): + return self.get_revisions()[idx] + def get_unstaged_item(self, idx): return self.get_all_unstaged()[idx] @@ -263,9 +269,9 @@ class Model(model.Model): else: status = 'Untracked, not staged' - file_type = git.run_cmd('file','-b',filename) + file_type = utils.run_cmd('file','-b',filename) if 'binary' in file_type or 'data' in file_type: - diff = git.run_cmd('hexdump','-C',filename) + diff = utils.run_cmd('hexdump','-C',filename) else: if os.path.exists(filename): file = open(filename, 'r') diff --git a/ugitlibs/pushcontroller.py b/ugitlibs/pushcontroller.py index 09069a2..22427d9 100644 --- a/ugitlibs/pushcontroller.py +++ b/ugitlibs/pushcontroller.py @@ -1,8 +1,17 @@ import os +from PyQt4.QtGui import QDialog -from qobserver import QObserver import utils import qtutils +from qobserver import QObserver +from views import PushDialog + +def push_branches(model, parent): + model = model.clone(init=False) + view = PushDialog(parent) + controller = PushController(model,view) + view.show() + return view.exec_() == QDialog.Accepted class PushController(QObserver): def __init__(self, model, view): @@ -11,10 +20,12 @@ class PushController(QObserver): self.model_to_view('remote', 'remoteText') self.model_to_view('remotes', 'remoteList') self.model_to_view('local_branch', 'localBranchText') - self.model_to_view('remote_branch', 'remoteBranchText') self.model_to_view('local_branches', 'localBranchList') + self.model_to_view('remote_branch', 'remoteBranchText') self.model_to_view('remote_branches', 'remoteBranchList') + self.add_actions('remotes', self.remotes) + self.add_signals('textChanged(const QString&)', view.remoteText, view.localBranchText, @@ -29,8 +40,6 @@ class PushController(QObserver): view.pushButton, view.cancelButton) - self.add_actions('remotes', self.remotes) - self.add_callbacks({ 'remoteList': self.remote_list, 'localBranchList': self.local_branch_list, @@ -39,7 +48,8 @@ class PushController(QObserver): }) self.connect(view.cancelButton, 'released()', view.reject) - model.notify_observers('remotes','local_branches','remote_branches') + model.notify_observers( + 'remotes','local_branches','remote_branches') def push(self): if not self.model.get_remote(): diff --git a/ugitlibs/repobrowsercontroller.py b/ugitlibs/repobrowsercontroller.py index a397ede..30e0123 100644 --- a/ugitlibs/repobrowsercontroller.py +++ b/ugitlibs/repobrowsercontroller.py @@ -1,9 +1,22 @@ #!/usr/bin/env python import os +from PyQt4.QtGui import QDialog from qobserver import QObserver import utils import qtutils import defaults +from views import CommitBrowser + +def browse_git_branch(model, parent, branch): + if not branch: return + # Clone the model to allow opening multiple browsers + # with different sets of data + model = model.clone(init=False) + model.set_branch(branch) + view = CommitBrowser(parent) + controller = RepoBrowserController(model, view) + view.show() + return view.exec_() == QDialog.Accepted class RepoBrowserController(QObserver): def __init__(self, model, view): diff --git a/ugitlibs/utilcontroller.py b/ugitlibs/utilcontroller.py new file mode 100644 index 0000000..fc8a1f9 --- /dev/null +++ b/ugitlibs/utilcontroller.py @@ -0,0 +1,69 @@ +#!/usr/bin/env python +from PyQt4.QtGui import QDialog +import qtutils +from qobserver import QObserver +from views import BranchDialog +from views import CommitBrowser + +def choose_branch(title, parent, branches): + dlg = BranchDialog(parent,branches) + dlg.setWindowTitle(dlg.tr(title)) + return dlg.get_selected() + +def select_commits(model, parent, revs, summaries): + '''Use the CommitBrowser to select commits from a list.''' + model = model.clone(init=False) + model.set_revisions(revs) + model.set_summaries(summaries) + view = CommitBrowser(parent) + ctl = SelectCommitsController(model, view) + return ctl.select_commits() + +class SelectCommitsController(QObserver): + def __init__(self, model, view): + QObserver.__init__(self, model, view) + self.connect(view.commitList, 'itemSelectionChanged()', + self.commit_sha1_selected ) + + def select_commits(self): + summaries = self.model.get_summaries() + if not summaries: + msg = self.tr('No commits exist in this branch.') + self.show_output(msg) + return([],[]) + + qtutils.set_items(self.view.commitList, summaries) + + self.view.show() + result = self.view.exec_() + if result != QDialog.Accepted: return([],[]) + + revs = self.model.get_revisions() + list_widget = self.view.commitList + selection = qtutils.get_selection_list(list_widget, revs) + if not selection: return([],[]) + + # also return the selected index numbers + index_nums = range(len(revs)) + idxs = qtutils.get_selection_list(list_widget, index_nums) + + return(selection, idxs) + + def commit_sha1_selected(self): + row, selected = qtutils.get_selected_row(self.view.commitList) + if not selected: + self.view.commitText.setText('') + self.view.revisionLine.setText('') + return + + # Get the sha1 and put it in the revision line + sha1 = self.model.get_revision_sha1(row) + self.view.revisionLine.setText(sha1) + self.view.revisionLine.selectAll() + + # Lookup the sha1's commit + commit_diff = self.model.diff(commit=sha1,cached=False) + self.view.commitText.setText(commit_diff) + + # Copy the sha1 into the clipboard + qtutils.set_clipboard(sha1) diff --git a/ugitlibs/utils.py b/ugitlibs/utils.py index 8e23d73..2eb56ff 100644 --- a/ugitlibs/utils.py +++ b/ugitlibs/utils.py @@ -3,10 +3,12 @@ import sys import os import re import time -import commands -import defaults from cStringIO import StringIO +from PyQt4.QtCore import QProcess + +import defaults + PREFIX = os.path.realpath(os.path.dirname(os.path.dirname(sys.argv[0]))) QMDIR = os.path.join(PREFIX, 'share', 'ugit', 'qm') @@ -38,8 +40,7 @@ KNOWN_FILE_TYPES = { def ident_file_type(filename): '''Returns an icon based on the contents of filename.''' if os.path.exists(filename): - quoted_filename = shell_quote(filename) - fileinfo = commands.getoutput('file -b %s' % quoted_filename) + fileinfo = run_cmd('file','-b',filename) for filetype, iconname in KNOWN_FILE_TYPES.iteritems(): if filetype in fileinfo.lower(): return iconname @@ -72,13 +73,44 @@ def get_directory_icon(): def get_file_icon(): return os.path.join(ICONSDIR, 'generic.png') +def run_cmd(cmd, *args, **kwargs): + # Handle cmd as either a string or an argv list + if type(cmd) is str: + cmd = cmd.split(' ') + cmd += list(args) + else: + cmd = list(cmd + list(args)) + + child = QProcess() + child.setProcessChannelMode(QProcess.MergedChannels); + child.start(cmd[0], cmd[1:]) + + if not child.waitForStarted(): raise Exception("failed to start child") + if not child.waitForFinished(): raise Exception("failed to start child") + + output = str(child.readAll()) + + # Allow run_cmd(argv, raw=True) for when we + # want the full, raw output(e.g. git cat-file) + if 'raw' in kwargs: + return output + else: + if 'with_status' in kwargs: + return child.exitCode(), output.rstrip() + else: + return output.rstrip() + def fork(*argv): pid = os.fork() if pid: return os.execlp(*argv) +__grep_cache = {} def grep(pattern, items, squash=True): - regex = re.compile(pattern) + if pattern in __grep_cache: + regex = __grep_cache[pattern] + else: + regex = __grep_cache[pattern] = re.compile(pattern) matched = [] for item in items: match = regex.match(item) diff --git a/ugitlibs/views.py b/ugitlibs/views.py index fbbf7df..6fd6eaa 100644 --- a/ugitlibs/views.py +++ b/ugitlibs/views.py @@ -1,6 +1,6 @@ import os -from PyQt4 import QtGui -from PyQt4.QtCore import SIGNAL +from PyQt4.QtGui import QDialog +from PyQt4.QtGui import QMainWindow from Window import Ui_Window from CommandDialog import Ui_CommandDialog from CommitBrowser import Ui_CommitBrowser @@ -10,13 +10,15 @@ from PushDialog import Ui_PushDialog from syntax import DiffSyntaxHighlighter import qtutils -class View(Ui_Window, QtGui.QMainWindow): +class View(Ui_Window, QMainWindow): '''The main ugit interface.''' def __init__(self, parent=None): - QtGui.QMainWindow.__init__(self, parent) + QMainWindow.__init__(self, parent) Ui_Window.__init__(self) self.setupUi(self) + DiffSyntaxHighlighter(self.displayText.document()) + # Qt does not handle noun/verb support self.commitButton.setText(qtutils.tr('Commit@@verb')) self.menuCommit.setTitle(qtutils.tr('Commit@@verb')) @@ -58,21 +60,22 @@ class View(Ui_Window, QtGui.QMainWindow): return offset, selection -class CommandDialog(Ui_CommandDialog, QtGui.QDialog): +class CommandDialog(Ui_CommandDialog, QDialog): '''A simple dialog to display command output.''' def __init__(self, parent=None, output=None): - QtGui.QDialog.__init__(self, parent) + QDialog.__init__(self, parent) Ui_CommandDialog.__init__(self) self.setupUi(self) - if output: self.set_command(output) + if output: self.set_output(output) - def set_command(self, output): + def set_output(self, output): self.commandText.setText(output) -class BranchDialog(Ui_BranchDialog, QtGui.QDialog): - '''A dialog to display available branches.''' - def __init__(self, parent, branches): - QtGui.QDialog.__init__(self, parent) +class BranchDialog(Ui_BranchDialog, QDialog): + '''A dialog for choosing branches.''' + + def __init__(self, parent=None, branches=None): + QDialog.__init__(self, parent) Ui_BranchDialog.__init__(self) self.setupUi(self) self.reset() @@ -89,37 +92,31 @@ class BranchDialog(Ui_BranchDialog, QtGui.QDialog): def get_selected(self): self.show() - if self.exec_() == QtGui.QDialog.Accepted: + if self.exec_() == QDialog.Accepted: return self.branches [ self.comboBox.currentIndex() ] else: return None - @staticmethod - def choose(title, parent, branches): - dlg = BranchDialog(parent,branches) - dlg.setWindowTitle(dlg.tr(title)) - return dlg.get_selected() - -class CommitBrowser(Ui_CommitBrowser, QtGui.QDialog): - '''A dialog to display commits in for selection.''' +class CommitBrowser(Ui_CommitBrowser, QDialog): + '''A dialog to display commits for selection.''' def __init__(self, parent=None): - QtGui.QDialog.__init__(self, parent) + QDialog.__init__(self, parent) Ui_CommitBrowser.__init__(self) self.setupUi(self) # Make the list widget slighty larger self.splitter.setSizes([ 50, 200 ]) DiffSyntaxHighlighter(self.commitText.document()) -class CreateBranchDialog(Ui_CreateBranchDialog, QtGui.QDialog): +class CreateBranchDialog(Ui_CreateBranchDialog, QDialog): '''A dialog for creating or updating branches.''' def __init__(self, parent=None): - QtGui.QDialog.__init__(self, parent) + QDialog.__init__(self, parent) Ui_CreateBranchDialog.__init__(self) self.setupUi(self) -class PushDialog(Ui_PushDialog, QtGui.QDialog): +class PushDialog(Ui_PushDialog, QDialog): '''A dialog for pushing branches.''' def __init__(self, parent=None): - QtGui.QDialog.__init__(self, parent) + QDialog.__init__(self, parent) Ui_PushDialog.__init__(self) self.setupUi(self) diff --git a/ui/Window.ui b/ui/Window.ui index 1ac90cb..28a2915 100644 --- a/ui/Window.ui +++ b/ui/Window.ui @@ -510,7 +510,7 @@ retrieve the latest commit message prior to committing. - Show Latest Diffstat + Show Diffstat Ctrl+D -- 2.11.4.GIT