[gnome-builder] contributing: start on contributing bootstrap



commit 22852f0dd9f40bfcff48728c85f518fc14c7ab90
Author: Christian Hergert <chergert redhat com>
Date:   Mon Dec 28 21:44:28 2015 -0800

    contributing: start on contributing bootstrap
    
    This is just the sketch of an idea for a contributing plugin. The idea is
    a few fold. First, we want to simplify the process of going from nothing
    to something as quick as possible. While we are focused on bootstraping
    GNOME projects, I don't see why this can't be used in a broader sense.
    
    This is just a minimal amount of code to get started. I'd love for someone
    to work on making this look more like the description in the IDEA file.
    
    We also need to figure out how we are going to hook into jhbuild/xdg-app
    to take care of ensuring we have necessary project dependencies.
    
    We also need to either extend DOAP or extract PKG_CHECK_MODULES() from
    configure.ac. We could also rely on modulesets to some degree.

 plugins/contributing/IDEA                   |   62 +++++++
 plugins/contributing/contributing.plugin    |   11 ++
 plugins/contributing/contributing_plugin.py |  229 +++++++++++++++++++++++++++
 plugins/contributing/helper.py              |  137 ++++++++++++++++
 4 files changed, 439 insertions(+), 0 deletions(-)
---
diff --git a/plugins/contributing/IDEA b/plugins/contributing/IDEA
new file mode 100644
index 0000000..dcfd0f3
--- /dev/null
+++ b/plugins/contributing/IDEA
@@ -0,0 +1,62 @@
+
+  ide contribute gnome-weather
+
+    - git clone gnome:gnome-weather
+    - cd gnome-weather
+    - determine build strategy
+      - xdg-app
+        - have build json description?
+        - download required sdk/runtime
+      - jhbuild
+        - can we locate a module set?
+        - bootstrap, build up to project in jhbuild
+    - build the project
+    - print contributing.md
+    - print information from doap, including maintainers
+
+  ide file-bug -p gnome-weather -m 'add foo support'
+  $EDITOR => enter bug description
+
+  ide release --version 3.20.0 --no-sign
+    - gnome-builder-3.20.0.tar.xz
+
+
+  - Cloning gnome-builder into ~/Projects
+    0:20 [=================>               ] 30Kb/sec
+  
+
+  ✔ Cloning gnome-builder into ~/Projects
+  - Locating project dependencies
+
+
+  ✔ Cloning gnome-builder into ~/Projects
+  ✔ Locating project dependencies
+  - Installing project dependencies
+    0:20 [=================>               ] 30Kb/sec
+
+
+  ✔ Cloning "gnome-builder" into ~/Projects
+  ✔ Locating "gnome-builder" dependencies
+  ✔ Installing "gnome-builder" dependencies
+  - Bootstraping "gnome-builder"
+
+
+
+
+
+  ✔ Cloning gnome-builder in ~/Projects
+  ✔ Locating project dependencies
+  ✔ Installing project dependencies
+  ✔ Bootstraping project
+
+  You are now ready to contribute to gnome-builder!
+
+  Report Issues: https://bugzilla.gnome.org/enter_bug.cgi?project=gnome-builder
+    Maintainers: Christian Hergert <christian hergert me>
+                 Christian Hergert <christian hergert me>
+   Mailing List: https://lists.gnome.org/Builder-list
+           Wiki: https://live.gnome.org/Apps/Builder
+            IRC: irc://irc.gimp.net/#gnome-builder
+
+  Type `make' to build gnome-builder
+
diff --git a/plugins/contributing/contributing.plugin b/plugins/contributing/contributing.plugin
new file mode 100644
index 0000000..3f56d83
--- /dev/null
+++ b/plugins/contributing/contributing.plugin
@@ -0,0 +1,11 @@
+[Plugin]
+Module=contributing_plugin
+Loader=python3
+Name=GNOME Contribute
+Description=Command line tool to bootstrap contributing to a GNOME project
+Authors=Christian Hergert <christian hergert me>
+Copyright=Copyright © 2015 Christian Hergert
+Builtin=true
+Hidden=true
+X-Tool-Name=contribute
+X-Tool-Description=Get started contributing to an existing GNOME project
diff --git a/plugins/contributing/contributing_plugin.py b/plugins/contributing/contributing_plugin.py
new file mode 100644
index 0000000..d1de4f3
--- /dev/null
+++ b/plugins/contributing/contributing_plugin.py
@@ -0,0 +1,229 @@
+#!/usr/bin/env python3
+
+#
+# contributing_plugin.py
+#
+# Copyright (C) 2015 Christian Hergert <chris dronelabs com>
+#
+# 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/>.
+#
+
+import gi
+from getopt import getopt, GetoptError
+import getpass
+from gettext import gettext as _
+import os
+import sys
+import time
+
+gi.require_version('Ggit', '1.0')
+
+import helper
+
+from gi.repository import GLib
+from gi.repository import GObject
+from gi.repository import Gio
+from gi.repository import Ggit
+from gi.repository import Ide
+
+class ContributeTool(helper.PyApplicationTool):
+    location = None
+
+    def do_run_async(self, args, cancellable, callback, user_data):
+        task = Gio.Task.new(self, cancellable, callback)
+
+        try:
+            self.parse(args)
+        except Exception as ex:
+            self.printerr(repr(ex))
+            task.return_int(1)
+            return
+
+        if not self.args:
+            self.printerr(_("Missing project name"))
+            self.printerr("")
+            self.usage(stream=sys.stderr)
+            task.return_int(1)
+            return
+
+        project_name = self.args[0]
+
+        self.clone(task, project_name)
+
+    def do_run_finish(self, result):
+        return result.propagate_int()
+
+    def usage(self, stream=sys.stdout):
+        stream.write(_("""Usage:
+  ide contribute PROJECT_NAME
+
+  This command will bootstrap your system to begin contributing to the project
+  denoted by PROJECT_NAME. This includes fetching the sources, ensuring that
+  you have the required dependencies to build, and bootstraps the first build
+  of the project.
+
+Examples:
+  ide contribute gnome-builder
+  ide contribute gnome-maps
+
+"""))
+
+    def clone(self, task, project_name):
+        callbacks = RemoteCallbacks(self)
+
+        fetch_options = Ggit.FetchOptions()
+        fetch_options.set_remote_callbacks(callbacks)
+
+        clone_options = Ggit.CloneOptions()
+        clone_options.set_is_bare(False)
+        clone_options.set_checkout_branch("master")
+        clone_options.set_fetch_options(fetch_options)
+
+        uri = self.get_project_uri(project_name)
+        self.location = self.get_project_dir(project_name)
+
+        self.write_line("Cloning %s into %s ..." % (project_name, self.location.get_path()))
+
+        try:
+            repository = Ggit.Repository.clone(uri, self.location, clone_options)
+        except Exception as ex:
+            self.printerr(ex.message)
+            task.return_int(1)
+            return
+
+        # TODO: Find project dependencies, install
+        #       that could be xdg-app runtime, jhbuild, etc
+        #self.write_line("Locating project dependencies")
+
+        # Load the project and try to build it.
+        Ide.Context.new_async(self.location,
+                              task.get_cancellable(),
+                              self.get_context_cb,
+                              task)
+
+    def get_project_uri(self, project_name):
+        # TODO: check jhbuildrc or ssh_config for username
+        return 'git://git.gnome.org/'+project_name+'.git'
+
+    def get_project_dir(self, project_name):
+        # TODO: configurable project dirs
+        parent_dir = os.path.join(GLib.get_home_dir(), 'Projects')
+
+        try:
+            os.makedirs(parent_dir)
+        except:
+            pass
+
+        return Gio.File.new_for_path(os.path.join(parent_dir, project_name))
+
+    def get_context_cb(self, object, result, task):
+        try:
+            context = Ide.Context.new_finish(result)
+        except Exception as ex:
+            self.printerr(ex.message)
+            task.return_int(1)
+            return
+
+        # Build the project
+        device_manager = context.get_device_manager()
+        device = device_manager.get_device('local')
+        build_system = context.get_build_system()
+        builder = build_system.get_builder(None, device)
+        result = builder.build_async(Ide.BuilderBuildFlags.NONE,
+                                     task.get_cancellable(),
+                                     self.build_cb, task)
+        result.connect('log', self.log_message)
+
+    def build_cb(self, builder, result, task):
+        try:
+            builder.build_finish(result)
+        except Exception as ex:
+            self.printerr(repr(ex))
+
+        contributing_md = self.location.get_child('CONTRIBUTING.md')
+        if contributing_md.query_exists():
+            (_, data, _) = contributing_md.load_contents()
+            print("\n\n\n")
+            print(data.decode('utf-8'))
+
+        task.return_int(0)
+
+    def log_message(self, result, stream, message):
+        if stream == Ide.BuildResultLog.STDOUT:
+            sys.stdout.write(message)
+        else:
+            sys.stderr.write(message)
+
+class RemoteCallbacks(Ggit.RemoteCallbacks):
+    done = False
+    started_at = 0
+
+    def __init__(self, tool, *args, **kwargs):
+        super().__init__(*args, **kwargs)
+        self.tool = tool
+
+    def do_transfer_progress(self, stats):
+        # Ignore callbacks after we finish progress
+        if self.done:
+            return
+
+        # Start tracking after first call to transfer_progress
+        if not self.started_at:
+            self.started_at = time.time()
+
+        fraction = stats.get_received_objects() / float(max(1, stats.get_total_objects()))
+
+        duration = time.time() - self.started_at
+        hours = duration / (60 * 60)
+        minutes = (duration % (60 * 60)) / 60
+        seconds = duration % 60
+        prefix = "%02u:%02u:%02u" % (hours, minutes, seconds)
+
+        rate = stats.get_received_bytes() / max(1, duration)
+        ratestr = GLib.format_size(rate)
+        if len(ratestr) < 8:
+            ratestr = (' ' * (8 - len(ratestr))) + ratestr
+
+        if fraction == 1.0:
+            transfered = GLib.format_size(stats.get_received_bytes())
+            self.tool.clear_line()
+            self.tool.write_progress(fraction, prefix, transfered)
+            self.tool.write_line("")
+            self.done = True
+        else:
+            self.tool.clear_line()
+            self.tool.write_progress(fraction, prefix, ratestr)
+
+    def do_credentials(self, url, username, allowed_creds):
+        if (allowed_creds & Ggit.Credtype.SSH_KEY) != 0:
+            username = self.get_username(username)
+            return Ggit.CredSshKeyFromAgent.new(username)
+
+        if (allowed_creds & Ggit.Credtype.SSH_INTERACTIVE) != 0:
+            username = self.get_username(username)
+            return Ggit.CredSshInteractive.new(username)
+
+        if (allowed_creds & Ggit.Credtype.USERPASS_PLAINTEXT) != 0:
+            username = self.get_username(username)
+            password = getpass.getpass()
+            return Ggit.CredPlaintext.new(username, password)
+
+        return None
+
+    def get_username(self, username):
+        if not username:
+            username = input('%s [%s]: ' % (_("Username"), getpass.getuser().strip()))
+            if not username:
+                username = getpass.getuser()
+        return username
diff --git a/plugins/contributing/helper.py b/plugins/contributing/helper.py
new file mode 100644
index 0000000..1aaabbf
--- /dev/null
+++ b/plugins/contributing/helper.py
@@ -0,0 +1,137 @@
+#!/usr/bin/env python3
+
+#
+# helper.py
+#
+# Copyright (C) 2015 Christian Hergert <chris dronelabs com>
+#
+# 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/>.
+#
+
+from getopt import getopt, GetoptError
+from gettext import gettext as _
+import shutil
+import sys
+
+from gi.repository import GObject
+from gi.repository import Gio
+from gi.repository import Ide
+
+class PyApplicationTool(GObject.Object, Ide.ApplicationTool):
+    # Leftover args during parsing
+    args = None
+
+    # Options extracted during parsing
+    options = None
+
+    # "ide"
+    prgname = None
+
+    # "build"
+    command = None
+
+    # getopt long and short options
+    long_opts = []
+    short_opts = ''
+
+    def parse(self, args):
+        args = list(args)
+        self.prgname = args.pop(0)
+        self.command = args.pop(0)
+        self.args = args
+
+        if 'h' not in self.short_opts:
+            self.short_opts += 'h'
+
+        if 'help' not in self.long_opts:
+            self.long_opts.append('help')
+
+        try:
+            self.options, self.args = getopt(args, self.short_opts, self.long_opts)
+
+            for o,a in self.options:
+                if o in ('-h', '--help'):
+                    self.usage(stream=sys.stdout)
+                    self.exit(0)
+                    return
+
+        except GetoptError:
+            self.usage(sys.stderr)
+
+    def usage(self, stream=sys.stdout):
+        stream.write(_("""Usage:
+  %(prgname) %(command) OPTIONS
+""" % {'prgname': self.prgname, 'command': self.command}))
+
+    def do_run_async(self, args, cancellable, callback, user_data):
+        task = Gio.Task(self, cancellable, callback)
+        task.return_int(0)
+
+    def do_run_finish(self, result):
+        return result.propagate_int()
+
+    def exit(self, exit_code):
+        sys.exit(exit_code)
+
+    def printerr(self, *args):
+        for arg in args:
+            sys.stderr.write(str(arg))
+            sys.stderr.write('\n')
+
+    def write_line(self, line):
+        sys.stdout.write(line)
+        sys.stdout.write('\n')
+
+    def clear_line(self):
+        sys.stdout.write("\033[2K")
+        sys.stdout.write("\r")
+
+    def write_progress(self, fraction, prefix="", suffix=""):
+        width = shutil.get_terminal_size().columns
+
+        sys.stdout.write("    ")
+        width -= 4
+
+        if prefix:
+            sys.stdout.write(prefix)
+            width -= len(prefix)
+
+            sys.stdout.write(" ")
+            width -= 1
+
+        if suffix:
+            width -= len(suffix)
+            width -= 1
+
+        sys.stdout.write(self._mkprog(fraction, width))
+
+        if suffix:
+            sys.stdout.write(" ")
+            sys.stdout.write(suffix)
+
+        sys.stdout.flush()
+
+    def _mkprog(self, fraction, width):
+        width -= 2 # [ and ]
+        fraction = min(1.0, max(0.0, fraction))
+        if fraction < 1.0:
+            width = max(4, width)
+            prog = '['
+            prog += '=' * (max(0, int(fraction * width) - 1))
+            prog += '>'
+            prog += max(0, (width - 2 - len(prog))) * ' '
+            prog += ']'
+            return prog
+        else:
+            return '[' + (width * '=') + ']'


[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]