Author | Tokens | Token Proportion | Commits | Commit Proportion |
---|---|---|---|---|
Oleksandr Andrushchenko | 3279 | 96.13% | 5 | 22.73% |
Thomas Zimmermann | 70 | 2.05% | 3 | 13.64% |
Sam Ravnborg | 15 | 0.44% | 1 | 4.55% |
Lv Yunlong | 10 | 0.29% | 1 | 4.55% |
Dan Carpenter | 8 | 0.23% | 2 | 9.09% |
Daniel Vetter | 7 | 0.21% | 3 | 13.64% |
Rob Herring | 6 | 0.18% | 1 | 4.55% |
Juergen Gross | 5 | 0.15% | 1 | 4.55% |
David Herrmann | 4 | 0.12% | 1 | 4.55% |
Javier Martinez Canillas | 3 | 0.09% | 1 | 4.55% |
Emil Velikov | 2 | 0.06% | 1 | 4.55% |
Colin Ian King | 1 | 0.03% | 1 | 4.55% |
Dawei Li | 1 | 0.03% | 1 | 4.55% |
Total | 3411 | 22 |
// SPDX-License-Identifier: GPL-2.0 OR MIT /* * Xen para-virtual DRM device * * Copyright (C) 2016-2018 EPAM Systems Inc. * * Author: Oleksandr Andrushchenko <oleksandr_andrushchenko@epam.com> */ #include <linux/delay.h> #include <linux/dma-mapping.h> #include <linux/module.h> #include <linux/of_device.h> #include <drm/drm_atomic_helper.h> #include <drm/drm_drv.h> #include <drm/drm_ioctl.h> #include <drm/drm_probe_helper.h> #include <drm/drm_file.h> #include <drm/drm_gem.h> #include <xen/platform_pci.h> #include <xen/xen.h> #include <xen/xenbus.h> #include <xen/xen-front-pgdir-shbuf.h> #include <xen/interface/io/displif.h> #include "xen_drm_front.h" #include "xen_drm_front_cfg.h" #include "xen_drm_front_evtchnl.h" #include "xen_drm_front_gem.h" #include "xen_drm_front_kms.h" struct xen_drm_front_dbuf { struct list_head list; u64 dbuf_cookie; u64 fb_cookie; struct xen_front_pgdir_shbuf shbuf; }; static void dbuf_add_to_list(struct xen_drm_front_info *front_info, struct xen_drm_front_dbuf *dbuf, u64 dbuf_cookie) { dbuf->dbuf_cookie = dbuf_cookie; list_add(&dbuf->list, &front_info->dbuf_list); } static struct xen_drm_front_dbuf *dbuf_get(struct list_head *dbuf_list, u64 dbuf_cookie) { struct xen_drm_front_dbuf *buf, *q; list_for_each_entry_safe(buf, q, dbuf_list, list) if (buf->dbuf_cookie == dbuf_cookie) return buf; return NULL; } static void dbuf_free(struct list_head *dbuf_list, u64 dbuf_cookie) { struct xen_drm_front_dbuf *buf, *q; list_for_each_entry_safe(buf, q, dbuf_list, list) if (buf->dbuf_cookie == dbuf_cookie) { list_del(&buf->list); xen_front_pgdir_shbuf_unmap(&buf->shbuf); xen_front_pgdir_shbuf_free(&buf->shbuf); kfree(buf); break; } } static void dbuf_free_all(struct list_head *dbuf_list) { struct xen_drm_front_dbuf *buf, *q; list_for_each_entry_safe(buf, q, dbuf_list, list) { list_del(&buf->list); xen_front_pgdir_shbuf_unmap(&buf->shbuf); xen_front_pgdir_shbuf_free(&buf->shbuf); kfree(buf); } } static struct xendispl_req * be_prepare_req(struct xen_drm_front_evtchnl *evtchnl, u8 operation) { struct xendispl_req *req; req = RING_GET_REQUEST(&evtchnl->u.req.ring, evtchnl->u.req.ring.req_prod_pvt); req->operation = operation; req->id = evtchnl->evt_next_id++; evtchnl->evt_id = req->id; return req; } static int be_stream_do_io(struct xen_drm_front_evtchnl *evtchnl, struct xendispl_req *req) { reinit_completion(&evtchnl->u.req.completion); if (unlikely(evtchnl->state != EVTCHNL_STATE_CONNECTED)) return -EIO; xen_drm_front_evtchnl_flush(evtchnl); return 0; } static int be_stream_wait_io(struct xen_drm_front_evtchnl *evtchnl) { if (wait_for_completion_timeout(&evtchnl->u.req.completion, msecs_to_jiffies(XEN_DRM_FRONT_WAIT_BACK_MS)) <= 0) return -ETIMEDOUT; return evtchnl->u.req.resp_status; } int xen_drm_front_mode_set(struct xen_drm_front_drm_pipeline *pipeline, u32 x, u32 y, u32 width, u32 height, u32 bpp, u64 fb_cookie) { struct xen_drm_front_evtchnl *evtchnl; struct xen_drm_front_info *front_info; struct xendispl_req *req; unsigned long flags; int ret; front_info = pipeline->drm_info->front_info; evtchnl = &front_info->evt_pairs[pipeline->index].req; if (unlikely(!evtchnl)) return -EIO; mutex_lock(&evtchnl->u.req.req_io_lock); spin_lock_irqsave(&front_info->io_lock, flags); req = be_prepare_req(evtchnl, XENDISPL_OP_SET_CONFIG); req->op.set_config.x = x; req->op.set_config.y = y; req->op.set_config.width = width; req->op.set_config.height = height; req->op.set_config.bpp = bpp; req->op.set_config.fb_cookie = fb_cookie; ret = be_stream_do_io(evtchnl, req); spin_unlock_irqrestore(&front_info->io_lock, flags); if (ret == 0) ret = be_stream_wait_io(evtchnl); mutex_unlock(&evtchnl->u.req.req_io_lock); return ret; } int xen_drm_front_dbuf_create(struct xen_drm_front_info *front_info, u64 dbuf_cookie, u32 width, u32 height, u32 bpp, u64 size, u32 offset, struct page **pages) { struct xen_drm_front_evtchnl *evtchnl; struct xen_drm_front_dbuf *dbuf; struct xendispl_req *req; struct xen_front_pgdir_shbuf_cfg buf_cfg; unsigned long flags; int ret; evtchnl = &front_info->evt_pairs[GENERIC_OP_EVT_CHNL].req; if (unlikely(!evtchnl)) return -EIO; dbuf = kzalloc(sizeof(*dbuf), GFP_KERNEL); if (!dbuf) return -ENOMEM; dbuf_add_to_list(front_info, dbuf, dbuf_cookie); memset(&buf_cfg, 0, sizeof(buf_cfg)); buf_cfg.xb_dev = front_info->xb_dev; buf_cfg.num_pages = DIV_ROUND_UP(size, PAGE_SIZE); buf_cfg.pages = pages; buf_cfg.pgdir = &dbuf->shbuf; buf_cfg.be_alloc = front_info->cfg.be_alloc; ret = xen_front_pgdir_shbuf_alloc(&buf_cfg); if (ret < 0) goto fail_shbuf_alloc; mutex_lock(&evtchnl->u.req.req_io_lock); spin_lock_irqsave(&front_info->io_lock, flags); req = be_prepare_req(evtchnl, XENDISPL_OP_DBUF_CREATE); req->op.dbuf_create.gref_directory = xen_front_pgdir_shbuf_get_dir_start(&dbuf->shbuf); req->op.dbuf_create.buffer_sz = size; req->op.dbuf_create.data_ofs = offset; req->op.dbuf_create.dbuf_cookie = dbuf_cookie; req->op.dbuf_create.width = width; req->op.dbuf_create.height = height; req->op.dbuf_create.bpp = bpp; if (buf_cfg.be_alloc) req->op.dbuf_create.flags |= XENDISPL_DBUF_FLG_REQ_ALLOC; ret = be_stream_do_io(evtchnl, req); spin_unlock_irqrestore(&front_info->io_lock, flags); if (ret < 0) goto fail; ret = be_stream_wait_io(evtchnl); if (ret < 0) goto fail; ret = xen_front_pgdir_shbuf_map(&dbuf->shbuf); if (ret < 0) goto fail; mutex_unlock(&evtchnl->u.req.req_io_lock); return 0; fail: mutex_unlock(&evtchnl->u.req.req_io_lock); fail_shbuf_alloc: dbuf_free(&front_info->dbuf_list, dbuf_cookie); return ret; } static int xen_drm_front_dbuf_destroy(struct xen_drm_front_info *front_info, u64 dbuf_cookie) { struct xen_drm_front_evtchnl *evtchnl; struct xendispl_req *req; unsigned long flags; bool be_alloc; int ret; evtchnl = &front_info->evt_pairs[GENERIC_OP_EVT_CHNL].req; if (unlikely(!evtchnl)) return -EIO; be_alloc = front_info->cfg.be_alloc; /* * For the backend allocated buffer release references now, so backend * can free the buffer. */ if (be_alloc) dbuf_free(&front_info->dbuf_list, dbuf_cookie); mutex_lock(&evtchnl->u.req.req_io_lock); spin_lock_irqsave(&front_info->io_lock, flags); req = be_prepare_req(evtchnl, XENDISPL_OP_DBUF_DESTROY); req->op.dbuf_destroy.dbuf_cookie = dbuf_cookie; ret = be_stream_do_io(evtchnl, req); spin_unlock_irqrestore(&front_info->io_lock, flags); if (ret == 0) ret = be_stream_wait_io(evtchnl); /* * Do this regardless of communication status with the backend: * if we cannot remove remote resources remove what we can locally. */ if (!be_alloc) dbuf_free(&front_info->dbuf_list, dbuf_cookie); mutex_unlock(&evtchnl->u.req.req_io_lock); return ret; } int xen_drm_front_fb_attach(struct xen_drm_front_info *front_info, u64 dbuf_cookie, u64 fb_cookie, u32 width, u32 height, u32 pixel_format) { struct xen_drm_front_evtchnl *evtchnl; struct xen_drm_front_dbuf *buf; struct xendispl_req *req; unsigned long flags; int ret; evtchnl = &front_info->evt_pairs[GENERIC_OP_EVT_CHNL].req; if (unlikely(!evtchnl)) return -EIO; buf = dbuf_get(&front_info->dbuf_list, dbuf_cookie); if (!buf) return -EINVAL; buf->fb_cookie = fb_cookie; mutex_lock(&evtchnl->u.req.req_io_lock); spin_lock_irqsave(&front_info->io_lock, flags); req = be_prepare_req(evtchnl, XENDISPL_OP_FB_ATTACH); req->op.fb_attach.dbuf_cookie = dbuf_cookie; req->op.fb_attach.fb_cookie = fb_cookie; req->op.fb_attach.width = width; req->op.fb_attach.height = height; req->op.fb_attach.pixel_format = pixel_format; ret = be_stream_do_io(evtchnl, req); spin_unlock_irqrestore(&front_info->io_lock, flags); if (ret == 0) ret = be_stream_wait_io(evtchnl); mutex_unlock(&evtchnl->u.req.req_io_lock); return ret; } int xen_drm_front_fb_detach(struct xen_drm_front_info *front_info, u64 fb_cookie) { struct xen_drm_front_evtchnl *evtchnl; struct xendispl_req *req; unsigned long flags; int ret; evtchnl = &front_info->evt_pairs[GENERIC_OP_EVT_CHNL].req; if (unlikely(!evtchnl)) return -EIO; mutex_lock(&evtchnl->u.req.req_io_lock); spin_lock_irqsave(&front_info->io_lock, flags); req = be_prepare_req(evtchnl, XENDISPL_OP_FB_DETACH); req->op.fb_detach.fb_cookie = fb_cookie; ret = be_stream_do_io(evtchnl, req); spin_unlock_irqrestore(&front_info->io_lock, flags); if (ret == 0) ret = be_stream_wait_io(evtchnl); mutex_unlock(&evtchnl->u.req.req_io_lock); return ret; } int xen_drm_front_page_flip(struct xen_drm_front_info *front_info, int conn_idx, u64 fb_cookie) { struct xen_drm_front_evtchnl *evtchnl; struct xendispl_req *req; unsigned long flags; int ret; if (unlikely(conn_idx >= front_info->num_evt_pairs)) return -EINVAL; evtchnl = &front_info->evt_pairs[conn_idx].req; mutex_lock(&evtchnl->u.req.req_io_lock); spin_lock_irqsave(&front_info->io_lock, flags); req = be_prepare_req(evtchnl, XENDISPL_OP_PG_FLIP); req->op.pg_flip.fb_cookie = fb_cookie; ret = be_stream_do_io(evtchnl, req); spin_unlock_irqrestore(&front_info->io_lock, flags); if (ret == 0) ret = be_stream_wait_io(evtchnl); mutex_unlock(&evtchnl->u.req.req_io_lock); return ret; } void xen_drm_front_on_frame_done(struct xen_drm_front_info *front_info, int conn_idx, u64 fb_cookie) { struct xen_drm_front_drm_info *drm_info = front_info->drm_info; if (unlikely(conn_idx >= front_info->cfg.num_connectors)) return; xen_drm_front_kms_on_frame_done(&drm_info->pipeline[conn_idx], fb_cookie); } void xen_drm_front_gem_object_free(struct drm_gem_object *obj) { struct xen_drm_front_drm_info *drm_info = obj->dev->dev_private; int idx; if (drm_dev_enter(obj->dev, &idx)) { xen_drm_front_dbuf_destroy(drm_info->front_info, xen_drm_front_dbuf_to_cookie(obj)); drm_dev_exit(idx); } else { dbuf_free(&drm_info->front_info->dbuf_list, xen_drm_front_dbuf_to_cookie(obj)); } xen_drm_front_gem_free_object_unlocked(obj); } static int xen_drm_drv_dumb_create(struct drm_file *filp, struct drm_device *dev, struct drm_mode_create_dumb *args) { struct xen_drm_front_drm_info *drm_info = dev->dev_private; struct drm_gem_object *obj; int ret; /* * Dumb creation is a two stage process: first we create a fully * constructed GEM object which is communicated to the backend, and * only after that we can create GEM's handle. This is done so, * because of the possible races: once you create a handle it becomes * immediately visible to user-space, so the latter can try accessing * object without pages etc. * For details also see drm_gem_handle_create */ args->pitch = DIV_ROUND_UP(args->width * args->bpp, 8); args->size = args->pitch * args->height; obj = xen_drm_front_gem_create(dev, args->size); if (IS_ERR(obj)) { ret = PTR_ERR(obj); goto fail; } ret = xen_drm_front_dbuf_create(drm_info->front_info, xen_drm_front_dbuf_to_cookie(obj), args->width, args->height, args->bpp, args->size, 0, xen_drm_front_gem_get_pages(obj)); if (ret) goto fail_backend; /* This is the tail of GEM object creation */ ret = drm_gem_handle_create(filp, obj, &args->handle); if (ret) goto fail_handle; /* Drop reference from allocate - handle holds it now */ drm_gem_object_put(obj); return 0; fail_handle: xen_drm_front_dbuf_destroy(drm_info->front_info, xen_drm_front_dbuf_to_cookie(obj)); fail_backend: /* drop reference from allocate */ drm_gem_object_put(obj); fail: DRM_ERROR("Failed to create dumb buffer: %d\n", ret); return ret; } static void xen_drm_drv_release(struct drm_device *dev) { struct xen_drm_front_drm_info *drm_info = dev->dev_private; struct xen_drm_front_info *front_info = drm_info->front_info; xen_drm_front_kms_fini(drm_info); drm_atomic_helper_shutdown(dev); drm_mode_config_cleanup(dev); if (front_info->cfg.be_alloc) xenbus_switch_state(front_info->xb_dev, XenbusStateInitialising); kfree(drm_info); } DEFINE_DRM_GEM_FOPS(xen_drm_dev_fops); static const struct drm_driver xen_drm_driver = { .driver_features = DRIVER_GEM | DRIVER_MODESET | DRIVER_ATOMIC, .release = xen_drm_drv_release, .prime_handle_to_fd = drm_gem_prime_handle_to_fd, .prime_fd_to_handle = drm_gem_prime_fd_to_handle, .gem_prime_import_sg_table = xen_drm_front_gem_import_sg_table, .gem_prime_mmap = drm_gem_prime_mmap, .dumb_create = xen_drm_drv_dumb_create, .fops = &xen_drm_dev_fops, .name = "xendrm-du", .desc = "Xen PV DRM Display Unit", .date = "20180221", .major = 1, .minor = 0, }; static int xen_drm_drv_init(struct xen_drm_front_info *front_info) { struct device *dev = &front_info->xb_dev->dev; struct xen_drm_front_drm_info *drm_info; struct drm_device *drm_dev; int ret; if (drm_firmware_drivers_only()) return -ENODEV; DRM_INFO("Creating %s\n", xen_drm_driver.desc); drm_info = kzalloc(sizeof(*drm_info), GFP_KERNEL); if (!drm_info) { ret = -ENOMEM; goto fail; } drm_info->front_info = front_info; front_info->drm_info = drm_info; drm_dev = drm_dev_alloc(&xen_drm_driver, dev); if (IS_ERR(drm_dev)) { ret = PTR_ERR(drm_dev); goto fail_dev; } drm_info->drm_dev = drm_dev; drm_dev->dev_private = drm_info; ret = xen_drm_front_kms_init(drm_info); if (ret) { DRM_ERROR("Failed to initialize DRM/KMS, ret %d\n", ret); goto fail_modeset; } ret = drm_dev_register(drm_dev, 0); if (ret) goto fail_register; DRM_INFO("Initialized %s %d.%d.%d %s on minor %d\n", xen_drm_driver.name, xen_drm_driver.major, xen_drm_driver.minor, xen_drm_driver.patchlevel, xen_drm_driver.date, drm_dev->primary->index); return 0; fail_register: drm_dev_unregister(drm_dev); fail_modeset: drm_kms_helper_poll_fini(drm_dev); drm_mode_config_cleanup(drm_dev); drm_dev_put(drm_dev); fail_dev: kfree(drm_info); front_info->drm_info = NULL; fail: return ret; } static void xen_drm_drv_fini(struct xen_drm_front_info *front_info) { struct xen_drm_front_drm_info *drm_info = front_info->drm_info; struct drm_device *dev; if (!drm_info) return; dev = drm_info->drm_dev; if (!dev) return; /* Nothing to do if device is already unplugged */ if (drm_dev_is_unplugged(dev)) return; drm_kms_helper_poll_fini(dev); drm_dev_unplug(dev); drm_dev_put(dev); front_info->drm_info = NULL; xen_drm_front_evtchnl_free_all(front_info); dbuf_free_all(&front_info->dbuf_list); /* * If we are not using backend allocated buffers, then tell the * backend we are ready to (re)initialize. Otherwise, wait for * drm_driver.release. */ if (!front_info->cfg.be_alloc) xenbus_switch_state(front_info->xb_dev, XenbusStateInitialising); } static int displback_initwait(struct xen_drm_front_info *front_info) { struct xen_drm_front_cfg *cfg = &front_info->cfg; int ret; cfg->front_info = front_info; ret = xen_drm_front_cfg_card(front_info, cfg); if (ret < 0) return ret; DRM_INFO("Have %d connector(s)\n", cfg->num_connectors); /* Create event channels for all connectors and publish */ ret = xen_drm_front_evtchnl_create_all(front_info); if (ret < 0) return ret; return xen_drm_front_evtchnl_publish_all(front_info); } static int displback_connect(struct xen_drm_front_info *front_info) { xen_drm_front_evtchnl_set_state(front_info, EVTCHNL_STATE_CONNECTED); return xen_drm_drv_init(front_info); } static void displback_disconnect(struct xen_drm_front_info *front_info) { if (!front_info->drm_info) return; /* Tell the backend to wait until we release the DRM driver. */ xenbus_switch_state(front_info->xb_dev, XenbusStateReconfiguring); xen_drm_drv_fini(front_info); } static void displback_changed(struct xenbus_device *xb_dev, enum xenbus_state backend_state) { struct xen_drm_front_info *front_info = dev_get_drvdata(&xb_dev->dev); int ret; DRM_DEBUG("Backend state is %s, front is %s\n", xenbus_strstate(backend_state), xenbus_strstate(xb_dev->state)); switch (backend_state) { case XenbusStateReconfiguring: case XenbusStateReconfigured: case XenbusStateInitialised: break; case XenbusStateInitialising: if (xb_dev->state == XenbusStateReconfiguring) break; /* recovering after backend unexpected closure */ displback_disconnect(front_info); break; case XenbusStateInitWait: if (xb_dev->state == XenbusStateReconfiguring) break; /* recovering after backend unexpected closure */ displback_disconnect(front_info); if (xb_dev->state != XenbusStateInitialising) break; ret = displback_initwait(front_info); if (ret < 0) xenbus_dev_fatal(xb_dev, ret, "initializing frontend"); else xenbus_switch_state(xb_dev, XenbusStateInitialised); break; case XenbusStateConnected: if (xb_dev->state != XenbusStateInitialised) break; ret = displback_connect(front_info); if (ret < 0) { displback_disconnect(front_info); xenbus_dev_fatal(xb_dev, ret, "connecting backend"); } else { xenbus_switch_state(xb_dev, XenbusStateConnected); } break; case XenbusStateClosing: /* * in this state backend starts freeing resources, * so let it go into closed state, so we can also * remove ours */ break; case XenbusStateUnknown: case XenbusStateClosed: if (xb_dev->state == XenbusStateClosed) break; displback_disconnect(front_info); break; } } static int xen_drv_probe(struct xenbus_device *xb_dev, const struct xenbus_device_id *id) { struct xen_drm_front_info *front_info; struct device *dev = &xb_dev->dev; int ret; ret = dma_coerce_mask_and_coherent(dev, DMA_BIT_MASK(64)); if (ret < 0) { DRM_ERROR("Cannot setup DMA mask, ret %d", ret); return ret; } front_info = devm_kzalloc(&xb_dev->dev, sizeof(*front_info), GFP_KERNEL); if (!front_info) return -ENOMEM; front_info->xb_dev = xb_dev; spin_lock_init(&front_info->io_lock); INIT_LIST_HEAD(&front_info->dbuf_list); dev_set_drvdata(&xb_dev->dev, front_info); return xenbus_switch_state(xb_dev, XenbusStateInitialising); } static void xen_drv_remove(struct xenbus_device *dev) { struct xen_drm_front_info *front_info = dev_get_drvdata(&dev->dev); int to = 100; xenbus_switch_state(dev, XenbusStateClosing); /* * On driver removal it is disconnected from XenBus, * so no backend state change events come via .otherend_changed * callback. This prevents us from exiting gracefully, e.g. * signaling the backend to free event channels, waiting for its * state to change to XenbusStateClosed and cleaning at our end. * Normally when front driver removed backend will finally go into * XenbusStateInitWait state. * * Workaround: read backend's state manually and wait with time-out. */ while ((xenbus_read_unsigned(front_info->xb_dev->otherend, "state", XenbusStateUnknown) != XenbusStateInitWait) && --to) msleep(10); if (!to) { unsigned int state; state = xenbus_read_unsigned(front_info->xb_dev->otherend, "state", XenbusStateUnknown); DRM_ERROR("Backend state is %s while removing driver\n", xenbus_strstate(state)); } xen_drm_drv_fini(front_info); xenbus_frontend_closed(dev); } static const struct xenbus_device_id xen_driver_ids[] = { { XENDISPL_DRIVER_NAME }, { "" } }; static struct xenbus_driver xen_driver = { .ids = xen_driver_ids, .probe = xen_drv_probe, .remove = xen_drv_remove, .otherend_changed = displback_changed, .not_essential = true, }; static int __init xen_drv_init(void) { /* At the moment we only support case with XEN_PAGE_SIZE == PAGE_SIZE */ if (XEN_PAGE_SIZE != PAGE_SIZE) { DRM_ERROR(XENDISPL_DRIVER_NAME ": different kernel and Xen page sizes are not supported: XEN_PAGE_SIZE (%lu) != PAGE_SIZE (%lu)\n", XEN_PAGE_SIZE, PAGE_SIZE); return -ENODEV; } if (!xen_domain()) return -ENODEV; if (!xen_has_pv_devices()) return -ENODEV; DRM_INFO("Registering XEN PV " XENDISPL_DRIVER_NAME "\n"); return xenbus_register_frontend(&xen_driver); } static void __exit xen_drv_fini(void) { DRM_INFO("Unregistering XEN PV " XENDISPL_DRIVER_NAME "\n"); xenbus_unregister_driver(&xen_driver); } module_init(xen_drv_init); module_exit(xen_drv_fini); MODULE_DESCRIPTION("Xen para-virtualized display device frontend"); MODULE_LICENSE("GPL"); MODULE_ALIAS("xen:" XENDISPL_DRIVER_NAME);
Information contained on this website is for historical information purposes only and does not indicate or represent copyright ownership.
Created with Cregit http://github.com/cregit/cregit
Version 2.0-RC1