~doctormo/groundcontrol/groundcontrol

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
#
# Copyright 2009 Martin Owens
#
# This program is free software: you can redistribute it and/or modify
#  it under the terms of the GNU General Public License as published by
#  the Free Software Foundation, either version 3 of the License, or
#  (at your option) any later version.
#
#  This program is distributed in the hope that it will be useful,
#  but WITHOUT ANY WARRANTY; without even the implied warranty of
#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#  GNU General Public License for more details.
#
#  You should have received a copy of the GNU General Public License
#  along with this program.  If not, see <http://www.gnu.org/licenses/>
#
"""
This is a simple extension for nautilus to allow project integration.
"""

import os
import gettext
import gtk
import gio
import gobject
import nautilus
import locale
import logging
import webbrowser

from urlparse import urlparse
from urllib import url2pathname
from bzrlib import errors

from GroundControl.projects import ProjectSelection, Project, ProjectCreateApp
from GroundControl.bugs     import BugSelection
from GroundControl.branches import BranchSelection
from GroundControl.commiter import CommitChanges, RevertChanges
from GroundControl.butties  import get_functions
from GroundControl.gtkviews import IconManager
from GroundControl.helper   import HelpApp
from GroundControl.merger   import RequestMergeApp, merge_url
from GroundControl.bazaar   import (
    BzrBranch, CommitStatusApp, PushStatusApp, BranchStatusApp,
    CheckoutStatusApp, PullStatusApp, RevertStatusApp, MergeStatusApp,
    UnlockStatusApp
)
from GroundControl.configuration import ConfigGui
from GroundControl.base import (
    PROJECT_XDG, PROJECT_NAME,
    is_online, set_online_changed
)
from GroundControl import __appname__, __version__
from GroundControl.launchpad import DEFAULT_CRED

# Do not use launchpadlib within this main thread
# It's not thread safe and causes lots of errors.

locale.setlocale(locale.LC_ALL, '')
gettext.install(__appname__, unicode=True)

print _("Initializing %s-%s extension" % (__appname__, __version__))
BIN_DIR = './bin'
HOME_DIR = os.path.expanduser('~/')
STATUS_ICONS = IconManager('')

class GroundControlBar(nautilus.LocationWidgetProvider, nautilus.InfoProvider):
    """Project management extension class"""
    def __init__(self):
        self._lpa    = None
        self._status = None
        self._giomon = None
        self._change_watch = False
        self._file_changed = False
        set_online_changed(self.update_widget)
        self._reset_vars()

    def _reset_vars(self):
        """Set some basic variables"""
        self._path    = None
        self._project = None
        self._widget  = None
        self._url     = None
        self._window  = None

    @property
    def status(self):
        """Get the status of the Launchpad configuration"""
        return os.path.exists(DEFAULT_CRED % PROJECT_NAME)

    def get_widget(self, url, window):
        """
        Returns either None or a Gtk widget to decorate the Nautilus
        window with, based on whether the current directory is a storage
        directory.
        """
        self._reset_vars()
        parsed_url = urlparse(url)
        self._url = url
        self._online = is_online()
        self._window = window

        if parsed_url.scheme == "file" and parsed_url.path:
            path = url2pathname(parsed_url.path)
            self._path = path
            self._widget = BarWidget(self, online=self._online)
            self._project = self.get_project(path)

            # Cancel any monitoring
            self.moniter_directory(None)
            # Convience variable for widget
            bar = self._widget
            config = os.path.join(path, '.groundcontrol')

            # Create the default file on contact, this isn't ideal
            # But it prepares the ground for more interesting features
            if path == os.path.join(HOME_DIR, _('Projects')):
                if not os.path.exists(config):
                    fhl = open(config, 'w')
                    fhl.write('\n')
                    fhl.close()
            
            # First part, the projects directory
            if os.path.exists(config):
                # Pre-functionality, check to see if our stuff works
                if not self.status:
                    msg = _("Please Enter Account Details")
                    if not self._online:
                        msg = _("Account Details Not Available")
                    bar.new_mode('configure', msg)
                    bar.button(_("Identify Yourself"), self.configure_gui)
                    bar.icon('launchpad')
                    return bar

                bar.new_mode('projects', _("Your Projects"))
                bar.button(_("Fetch Project"), self.project_gui)
                bar.button(_("Fix Bug"), self.bugfix_gui)
                bar.icon('projects')
                return bar

            # Second part, an actual project
            if self._project:
                if self._project.broken:
                    # This means the file borked and needs to be regenerated
                    bar.new_mode('project', "Project Configuration Corrupted")
                    if self._online:
                        bar.button(_("Get New Data"), self._project.regenerate,
                            self._window, self.update_widget)
                    bar.icon('groundcontrol')
                else:
                    bar.new_mode('project', "%s" % self._project.name)
                    if self._online:
                        bar.button(_("Fetch Branch"), self.branch_gui)
                        bar.button(_("Fix Bug"), self.bugfix_gui,
                            self._project.pid)
                    bar.icon(self._project.logo())
                return bar

            # Third part, inside a code branch
            if os.path.exists(os.path.join(path, '.bzr')):
                try:
                    return self.bazaar_choices(path, bar)
                except errors.BzrError, error:
                    message = str(error)
                    logging.warn("Found a broken bazaar branch! %s" % message)
                    bar.new_mode('broken', _('Broken Bzr Branch %s' % message))
                    bar.icon('error')
                    return bar
        return None

    def hide_widget(self):
        """We always need a bar widget, but we sometimes need to hide it"""
        #logging.debug("TRYING TO HIDE %s" % self._widget.get_parent())
        self._widget.get_parent().hide()

    def update_widget(self, widget=None):
        """Refresh the bar widget"""
        if not self._widget:
            return
        self._status = None
        oldparent = self._widget.get_parent()
        self._widget.destroy()
        logging.debug("Updated: Getting a new Nautilus widget")
        new_widget = self.get_widget(self._url, self._window)
        if new_widget:
            oldparent.pack_start(new_widget)
            oldparent.show()
        else:
            oldparent.hide()

    def get_project(self, path):
        """Get project information for a dir"""
        try:
            return Project(path)
        except IOError:
            return None

    def configure_gui(self, widget=None):
        """Reconfigure the launchpad oauth login"""
        try:
            ConfigGui(
                parent=self._window,
                callback=self.update_widget,
                start_loop=True)
        except Exception, message:
            logging.error(_("Error in config GUI: %s") % message)

    def project_gui(self, widget=None):
        """Calls up the project selection gui"""
        ProjectSelection(self._path,
            callback=self.do_project_create,
            parent=self._window,
            start_loop=True)

    def do_project_create(self, project):
        """Create a new project with our project object"""
        ProjectCreateApp(path=self._path,
            project=project,
            parent=self._window,
            start_loop=True)

    def bugfix_gui(self, widget=None, project=None):
        """Calls up the bug selection gui"""
        BugSelection(project, ensure_project=self._path,
            callback=self.create_bugfix,
            parent=self._window,
            start_loop=True)

    def create_bugfix(self, bug, project=None):
        """Prepare to fix a bug we've selected"""
        self._working_bug = bug
        path = self._path
        if not project:
            path = os.path.join(self._path, bug.project_id)
        if os.path.exists(path):
            # Load project without touching bug.project because that
            # would take more time to load the object and it's attr.
            project = Project(path)
        else:
            # Error because this shouldn't happen
            raise Exception("Couldn't create projects directory!!")
        # We need a generated workname and branch name
        lpname = 'lp:%s' % project.pid
        workname = 'bugfix-lp-%s' % bug.id
        # At first we try a default development focus
        try:
            branch = BzrBranch(lpname)
            logging.debug("Found development target %s" % branch.url)
        except errors.InvalidURL:
            # Now that we know we don't have a development focus
            # Try and ask the user to choose a branch.
            BranchSelection(project.pid, path,
                workname=workname,
                parent=self._window,
                callback=self.create_fix_branch,
                start_loop=True)
        else:
            # Branch a new branch for our code using the development focus
            self.create_fix_branch(lpname=lpname, path=path, workname=workname)

    def create_fix_branch(self, **kwargs):
        """A special fix branch which is labeled as such"""
        path = os.path.join(kwargs['path'], kwargs['workname'])
        self.do_branch(**kwargs)
        fixes = self._working_bug.id
        title = self._working_bug.name
        bra = BzrBranch(path)
        bra.config.set_user_option('fixes', fixes)
        bra.config.set_user_option('bug_title', title)

    def branch_gui(self, widget=None):
        """Load the Branch Selecter GUI"""
        if self._project.pid:
            BranchSelection(self._project.pid, self._path,
                parent=self._window,
                callback=self.do_branch,
                start_loop=True)

    def do_branch(self, **kwargs):
        """Load a new window to deal with branching."""
        path = kwargs.pop('path', self._path)
        lpname = kwargs.pop('lpname')
        workname = kwargs.pop('workname')
        # No need to callback to refresh, we're in the wrong dir
        if not lpname:
            raise Exception("Can't fetch code, no branch specified")

        if workname:
            logging.debug("Branching Code...")
            BranchStatusApp(branch=lpname, path=path,
                workname=workname,
                parent=self._window,
                start_loop=True)
        else:
            logging.debug("Checking-out Code...")
            CheckoutStatusApp(branch=lpname, path=path,
                parent=self._window,
                start_loop=True)

    def bazaar_choices(self, path, widget):
        """Decide what choices to give to users inside a bazaar project"""
        brch = BzrBranch(path)
        project = self.get_project(os.path.dirname(path))
        self.moniter_directory(path)
        # Decide if we have a branch checkout (it has a push setup)
        if brch.has_branching() and brch.branch.get_push_location():
            fixes    = brch.config.get_user_option('fixes')
            bugtitle = brch.config.get_user_option('bug_title')
            changes  = brch.get_changes()
            if brch.is_locked():
                widget.new_mode('code-locked', _("Branch Locked"))
                widget.button(_("Unlock"), self.unlock_gui,
                    brch, offline=True)
            # Elsif there are non-commited changes
            elif changes.has_changed():
                widget.new_mode('code-modified', _("Files Modified"))
                widget.button(_("Commit Changes"), self.commit_gui,
                    brch, project, offline=True)
                widget.button(_("Revert Changes"), self.revert_gui,
                    brch, offline=True)
            # Elsif there are new files, we all want to commit them.
            elif brch.has_newfiles():
                widget.new_mode('code-modified', _("New Files"))
                widget.button(_("Commit New Files"), self.commit_gui,
                    brch, project, offline=True)
                widget.button(_("Revert Changes"), self.revert_gui,
                    brch, offline=True)
            # Elseif there is a difference between remote and local versions
            elif brch.has_commits():
                widget.new_mode('code-commited', _("Local Changes"))
                widget.button(_("Upload Changes"), self.do_push, brch)
                widget.button(_("Update"), self.do_resync, brch)
            # Elsif there has already been a merge request posted.
            elif brch.merge_revno():
                widget.new_mode('code-mergereq', _("Merge Requested"))
                widget.button(_("View Request"), self.view_merge, brch)
                #widget.button("Update Status", self.update_merge, brch)
            # Elsif there is no difference, but it's bigger
            # than the original version
            elif brch.has_pushes() and brch.is_child():
                widget.new_mode('code-uploaded',
                    _("Uploaded - Merge Required"))
                widget.button(_("Request Merge"), self.do_merge_request, brch)
                widget.button(_("Update"), self.do_resync, brch)
            else:
                # If there is nothing to do, then don't display anything
                #gobject.timeout_add( 200, self.hide_widget )
                # But there is something to do, do an update to trunk
                widget.new_mode('code-none', _("Code Branch"))
                widget.button(_("Update"), self.do_resync, brch)
                if project and not brch.is_child():
                    widget.button(_("Merge In"), self.merge_gui, brch, project)
            # We only want to display a single response for bug fixes
            # Except for when we have done our merge request.
            if fixes and not brch.merge_revno():
                mode = widget._mode_id
                title = "[lp:%s] %s" % (fixes, bugtitle)
                if mode == 'code-none':
                    widget.new_mode('code-none', title)
                elif mode != 'code-mergereq':
                    widget.new_mode('code-modified', title)
                    widget.button(_("Upload Fix"), self.submitfix_gui,
                        brch, project, fixes)
                widget.icon('fix-bug')
                self.add_custom_functions(widget, path)
                return widget
        else:
            widget.new_mode('code-checkout', _("<b>Read-Only Branch</b>"))
            widget.button(_("Update to Latest"), self.do_resync, brch)
        self.add_custom_functions(widget, path)
        widget.icon('bazaar')
        return widget

    def unlock_gui(self, widget, branch):
        """Show the confirm commit dialog"""
        UnlockStatusApp(callback=self.update_widget,
                parent=self._window,
                start_loop=True,
                branch=branch)

    def submitfix_gui(self, widget, branch, project, fixing):
        """Show the confirm commit dialog"""
        if branch.get_changes().has_changed():
            # Only commit if we have changes to commit
            CommitChanges(branch=branch, project=project, fixing=fixing,
                callback=self.do_submitfix,
                parent=self._window,
                start_loop=True)
        else:
            # Move on to what's needed to do
            self.do_mergefix(branch)

    def do_submitfix(self, branch, **kwargs):
        """Submit a fix by commiting then merge the fix"""
        self.do_commit(branch=branch, **kwargs)
        self.do_mergefix(branch)

    def do_mergefix(self, branch):
        """Make a merge request if we don't already have one"""
        if not branch.merge_revno():
            self.do_push(None, branch)
            self.do_merge_request(None, branch)
        else:
            self.do_push(None, branch)

    def commit_gui(self, widget, branch, project):
        """Show the confirm commit dialog"""
        CommitChanges(branch=branch, project=project,
                callback=self.do_commit,
                parent=self._window,
                start_loop=True)

    def do_commit(self, **kwargs):
        """What to do after we've commited"""
        CommitStatusApp(callback=self.update_widget,
                parent=self._window,
                start_loop=True,
                **kwargs)

    def revert_gui(self, widget, branch):
        """Ask if we should revert the changes."""
        RevertChanges(branch=branch,
                parent=self._window,
                callback=self.do_revert,
                start_loop=True)

    def do_revert(self, **kwargs):
        """Now actually revert the branch"""
        RevertStatusApp(callback=self.update_widget,
                parent=self._window,
                start_loop=True,
                **kwargs)

    def do_resync(self, widget, branch):
        """Do a pull and then update out widget"""
        PullStatusApp(branch=branch,
                callback=self.update_widget,
                parent=self._window,
                start_loop=True)

    def do_push(self, widget, branch):
        """Do a push and then update our widget"""
        PushStatusApp(branch=branch,
                callback=self.update_widget,
                parent=self._window,
                start_loop=True)

    def do_merge_request(self, widget, branch):
        """Open the gui for creating a merge request"""
        RequestMergeApp(branch,
                parent=self._window,
                callback=self.update_widget,
                start_loop=True)

    def view_merge(self, widget, branch):
        """Somehow view the merge request, maybe open up the web browser"""
        webbrowser.open(merge_url(branch), autoraise=1)

    def update_merge(self, widget, branch):
        """Updates the merge request to check if it's done or not."""
        logging.warn(_("Functionality not written yet"))

    def merge_gui(self, widget, branch, project):
        """Show a list of branches for which to merge into this one"""
        if project.pid:
            BranchSelection(project.pid, self._path,
                workname='#merge',
                branch=branch,
                parent=self._window,
                callback=self.do_merge,
                start_loop=True)

    def do_merge(self, **kwargs):
        """Merge the given branch with the branch at path"""
        path = kwargs.pop('path', self._path)
        lpname = kwargs.pop('lpname')
        MergeStatusApp(branch=path, source=lpname,
                callback=self.update_widget,
                parent=self._window,
                start_loop=True)

    def update_file_info(self, file):
        """will hopefully let us update icons and such"""
        if file.get_uri_scheme() == 'file':
            path = url2pathname(urlparse(file.get_uri()).path)
            if os.path.exists(os.path.join(path, '.gcproject')):
                file.add_emblem('package')
                #file.add_string_attribute('custom_icon', '.logo.png')
            elif os.path.exists(os.path.join(path, '.bzr')):
                file.add_emblem('development')

    def moniter_directory(self, path):
        """Moniter one directory for changes, cancel any previous"""
        if self._giomon:
            self._giomon.cancel()
        if path:
            gfile = gio.file_parse_name(path)
            self._giomon = gfile.monitor_directory()
            self._giomon.connect("changed", self.file_changed)
            if not self._change_watch:
                gobject.timeout_add( 1000, self.check_file_changes )
                self._change_watch = True

    def file_changed(self, filemonitor, file, other_file, event_type):
        """Event for when files have changed"""
        self._file_changed = True

    def check_file_changes(self):
        """This is make changes more course and not so many of them"""
        if self._file_changed:
            self._file_changed = False
            logging.debug("Bazaar contents have changed - updating...")
            self.update_widget()
        if self._window and self._window.get_window():
            gobject.timeout_add( 1000, self.check_file_changes )

    def add_custom_functions(self, bar, path):
        """Add some buttons to the bar widget"""
        cmode = bar._mode_id
        for cmd in get_functions(path):
            if cmd.is_mode(cmode):
                ofl = cmd.opt.get('offline', 'True') == 'True'
                button = bar.button(None, cmd.run, offline=ofl)
                label = gtk.Label()
                label.set_markup("<i>%s</i>" % cmd.label)
                label.show()
                button.add(label)
                button.show()


class BarWidget(gtk.HBox):
    """Basic bar widget for location widgets"""
    def __init__(self, parent, *args, **kwargs):
        """Return a valid wiget for this box"""
        self._online = kwargs.pop('online', True)
        super(BarWidget, self).__init__(*args, **kwargs)
        self._bar = parent
        self._icon = None
        self._mode_id = None
        self._attached = []
        self._label = gtk.Label()
        self._label.set_alignment(0.0, 0.5)
        self._label.set_line_wrap(False)
        self.pack_start(self._label, expand=True, fill=True, padding=4)
        self.show()

    def new_mode(self, mode_id, label):
        """Clears all the existing settings and replaces the label"""
        self._mode_id = mode_id
        self._label.set_markup("<b>%s</b>" % label)
        self._label.show()
        for att in self._attached:
            self.remove(att)
        self._attached = []
        if self._icon:
            self.remove(self._icon)
            self._icon = None
        self.show_help_button()
        gobject.timeout_add( 200, self.show_parent )

    def show_parent(self):
        """show the widgets parent object"""
        parent = self.get_parent()
        if parent:
            self.get_parent().show()
        else:
            logging.debug("Couldn't show bar parent widget - not packed yet")

    def icon(self, iconname):
        """Sets a single location for an image icon on the line."""
        if not self._icon:
            self._icon = gtk.Image()
            self.pack_start(self._icon, expand=False, fill=False, padding=4)
            self.reorder_child(self._icon, 0)
        pixbuf = STATUS_ICONS.get_icon(iconname)
        if pixbuf:
            endresult = pixbuf.scale_simple(32, 32, gtk.gdk.INTERP_BILINEAR)
            self._icon.set_from_pixbuf(endresult)
            self._icon.show()

    def button(self, label, signal, *attrs, **kwargs):
        """Adds a new button to the bar widget and attaches a signal."""
        offline = kwargs.get('offline', False)
        if not (offline or self._online):
            return None
        button = gtk.Button()
        button.connect("clicked", signal, *attrs)
        if label:
            button.set_label(label)
            button.show()
        self._attached.append(button)
        self.pack_end(button, expand=False, fill=False, padding=4)
        return button

    def show_help_button(self):
        """Add in a helpful widget that runs show_help"""
        if HelpApp.is_help(self._mode_id):
            image = gtk.Image()
            pixbuf = image.render_icon(gtk.STOCK_HELP, gtk.ICON_SIZE_BUTTON)
            image.set_from_pixbuf(pixbuf)
            image.show()
            button = self.button(None, self.show_help, offline=True)
            button.set_relief(gtk.RELIEF_NONE)
            button.add(image)
            button.show()

    def show_help(self, widget=None):
        """Show a simple icon button that shows help when clicked"""
        if HelpApp.is_help(self._mode_id):
            HelpApp(self._mode_id, parent=self._bar._window, start_loop=True)