mirror of
https://github.com/klaussilveira/gitlist.git
synced 2026-07-05 11:08:18 +02:00
Merge branch 'master' of https://github.com/klaussilveira/gitlist
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -31,3 +31,4 @@ node_modules
|
||||
config.ini
|
||||
cache.properties
|
||||
composer.phar
|
||||
phpunit.xml
|
||||
|
||||
@@ -2,8 +2,10 @@
|
||||
Options -MultiViews
|
||||
|
||||
RewriteEngine On
|
||||
#RewriteBase /path/to/gitlist/
|
||||
|
||||
RewriteCond %{REQUEST_FILENAME} !-f
|
||||
RewriteRule ^(.*)$ /index.php [L,NC]
|
||||
RewriteRule ^(.*)$ index.php [L,NC]
|
||||
</IfModule>
|
||||
<Files config.ini>
|
||||
order allow,deny
|
||||
|
||||
18
INSTALL.md
18
INSTALL.md
@@ -1,5 +1,5 @@
|
||||
# GitList Installation
|
||||
* Download GitList from [gitlist.org](http://gitlist.org/) and decompress to your `/var/www/gitlist` folder, or anywhere else you want to place GitList.
|
||||
* Download GitList from [gitlist.org](http://gitlist.org/) and decompress to your `/var/www/gitlist` folder, or anywhere else you want to place GitList.
|
||||
* Rename the `config.ini-example` file to `config.ini`.
|
||||
* Open up the `config.ini` and configure your installation. You'll have to provide where your repositories are located and the base GitList URL (in our case, http://localhost/gitlist).
|
||||
* Create the cache folder and give read/write permissions to your web server user:
|
||||
@@ -13,7 +13,7 @@ chmod 777 cache
|
||||
That's it, installation complete!
|
||||
|
||||
## Webserver configuration
|
||||
Apache is the "default" webserver for GitList. You will find the configuration inside the `.htaccess` file. However, nginx and lighttpd are also supported.
|
||||
Apache is the "default" webserver for GitList. You will find the configuration inside the `.htaccess` file. However, nginx and lighttpd are also supported.
|
||||
|
||||
### nginx server.conf
|
||||
|
||||
@@ -73,4 +73,16 @@ url.rewrite-once = (
|
||||
"^/gitlist/favicon\.ico$" => "$0",
|
||||
"^/gitlist(/[^\?]*)(\?.*)?" => "/gitlist/index.php$1$2"
|
||||
)
|
||||
```
|
||||
```
|
||||
|
||||
### hiawatha
|
||||
|
||||
```
|
||||
UrlToolkit {
|
||||
ToolkitID = gitlist
|
||||
RequestURI isfile Return
|
||||
# If you have example.com/gitlist/ ; Otherwise remove "/gitlist" below
|
||||
Match ^/gitlist/.* Rewrite /gitlist/index.php
|
||||
Match ^/gitlist/.*\.ini DenyAccess
|
||||
}
|
||||
```
|
||||
|
||||
7
boot.php
7
boot.php
@@ -1,11 +1,5 @@
|
||||
<?php
|
||||
|
||||
if (!isset($config)) {
|
||||
die("No configuration object provided.");
|
||||
}
|
||||
|
||||
$config->set('git', 'repositories', rtrim($config->get('git', 'repositories'), DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR);
|
||||
|
||||
// Startup and configure Silex application
|
||||
$app = new GitList\Application($config, __DIR__);
|
||||
|
||||
@@ -14,5 +8,6 @@ $app->mount('', new GitList\Controller\MainController());
|
||||
$app->mount('', new GitList\Controller\BlobController());
|
||||
$app->mount('', new GitList\Controller\CommitController());
|
||||
$app->mount('', new GitList\Controller\TreeController());
|
||||
$app->mount('', new GitList\Controller\NetworkController());
|
||||
|
||||
return $app;
|
||||
|
||||
45
build.xml
45
build.xml
@@ -1,6 +1,7 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project name="gitlist" default="build">
|
||||
<target name="build" depends="prepare,lint,phploc,pdepend,phpmd,phpcpd,phpunit,package" />
|
||||
<target name="build" depends="prepare,lint,phploc,pdepend,phpmd,phpcpd,phpunit,phpcs" />
|
||||
<target name="build-package" depends="prepare-package,package" />
|
||||
|
||||
<target name="clean" description="Clean build artifacts">
|
||||
<delete dir="${basedir}/build"/>
|
||||
@@ -9,12 +10,21 @@
|
||||
<target name="prepare" depends="clean" description="Prepare for build">
|
||||
<mkdir dir="${basedir}/build/logs"/>
|
||||
<mkdir dir="${basedir}/build/pdepend"/>
|
||||
<copy file="config.ini-example" tofile="config.ini"/>
|
||||
<exec executable="composer" failonerror="true">
|
||||
<arg value="install" />
|
||||
<arg value="--dev" />
|
||||
</exec>
|
||||
</target>
|
||||
|
||||
<target name="prepare-package" description="Prepare for build">
|
||||
<delete dir="${basedir}/vendor"/>
|
||||
<exec executable="composer" failonerror="true">
|
||||
<arg value="install" />
|
||||
<arg value="--optimize-autoloader" />
|
||||
</exec>
|
||||
</target>
|
||||
|
||||
<target name="lint" description="Perform syntax check of sourcecode files">
|
||||
<apply executable="php" failonerror="true">
|
||||
<arg value="-l" />
|
||||
@@ -32,9 +42,7 @@
|
||||
<exec executable="phploc">
|
||||
<arg value="--log-csv" />
|
||||
<arg value="${basedir}/build/logs/phploc.csv" />
|
||||
<arg value="--exclude" />
|
||||
<arg value="${basedir}/cache,${basedir}/vendor" />
|
||||
<arg path="${basedir}/" />
|
||||
<arg path="${basedir}/src" />
|
||||
</exec>
|
||||
</target>
|
||||
|
||||
@@ -44,19 +52,17 @@
|
||||
<arg value="--jdepend-chart=${basedir}/build/pdepend/dependencies.svg" />
|
||||
<arg value="--overview-pyramid=${basedir}/build/pdepend/overview-pyramid.svg" />
|
||||
<arg value="--ignore=${basedir}/cache,${basedir}/vendor" />
|
||||
<arg path="${basedir}/" />
|
||||
<arg path="${basedir}/src" />
|
||||
</exec>
|
||||
</target>
|
||||
|
||||
<target name="phpmd" description="Perform project mess detection using PHPMD creating a log file for the continuous integration server">
|
||||
<exec executable="phpmd">
|
||||
<arg path="${basedir}/" />
|
||||
<arg path="${basedir}/src" />
|
||||
<arg value="xml" />
|
||||
<arg value="codesize,design,unusedcode,naming" />
|
||||
<arg value="--reportfile" />
|
||||
<arg value="${basedir}/build/logs/pmd.xml" />
|
||||
<arg value="--exclude" />
|
||||
<arg value="${basedir}/cache,${basedir}/vendor" />
|
||||
</exec>
|
||||
</target>
|
||||
|
||||
@@ -64,9 +70,7 @@
|
||||
<exec executable="phpcpd">
|
||||
<arg value="--log-pmd" />
|
||||
<arg value="${basedir}/build/logs/pmd-cpd.xml" />
|
||||
<arg value="--exclude" />
|
||||
<arg value="${basedir}/cache,${basedir}/vendor" />
|
||||
<arg path="${basedir}/" />
|
||||
<arg path="${basedir}/src" />
|
||||
</exec>
|
||||
</target>
|
||||
|
||||
@@ -74,12 +78,25 @@
|
||||
<exec executable="phpunit" failonerror="true"/>
|
||||
</target>
|
||||
|
||||
<target name="phpcs" description="Find coding standard violations using PHP_CodeSniffer creating a log file for the continuous integration server">
|
||||
<exec executable="phpcs" output="/dev/null">
|
||||
<arg value="--report=checkstyle" />
|
||||
<arg value="--report-file=${basedir}/build/logs/checkstyle.xml" />
|
||||
<arg value="--standard=PSR2" />
|
||||
<arg path="${basedir}/src" />
|
||||
</exec>
|
||||
</target>
|
||||
|
||||
<target name="package" description="Package the application for distribution">
|
||||
<tar destfile="${basedir}/build/gitlist.tar.gz"
|
||||
basedir="${basedir}/"
|
||||
excludes="build/**, tests/**, phpunit.xml.dist, cache.properties, .gitignore, .travis.yml, build.xml, composer.json, composer.lock, config.ini"
|
||||
<copy todir="${basedir}/build/gitlist/">
|
||||
<fileset dir="${basedir}" excludes="cache/**, build/**, tests/**, pkg_builder/**, phpunit.xml.dist, cache.properties, .gitignore, .travis.yml, build.xml, composer.json, composer.lock, config.ini" />
|
||||
</copy>
|
||||
|
||||
<tar destfile="${basedir}/build/gitlist-master.tar.gz"
|
||||
basedir="${basedir}/build/"
|
||||
compression="gzip"
|
||||
longfile="gnu"
|
||||
excludes="gitlist-master.tar.gz, **/logs/**, **/pdepend/**"
|
||||
/>
|
||||
</target>
|
||||
</project>
|
||||
|
||||
@@ -1,15 +1,18 @@
|
||||
{
|
||||
"name": "klaussilveira/gitlist",
|
||||
"require": {
|
||||
"silex/silex": "1.0.*",
|
||||
"twig/twig": "1.9.*",
|
||||
"symfony/twig-bridge": "2.1.*",
|
||||
"symfony/filesystem": "2.1.*",
|
||||
"klaussilveira/gitter": "dev-master"
|
||||
"silex/silex": "1.0.*@dev",
|
||||
"twig/twig": "1.12.*",
|
||||
"symfony/twig-bridge": "2.2.*",
|
||||
"symfony/filesystem": "2.2.*",
|
||||
"klaussilveira/gitter": "0.2.*"
|
||||
},
|
||||
"require-dev": {
|
||||
"symfony/browser-kit": "2.1.*",
|
||||
"symfony/css-selector": "2.1.*"
|
||||
"symfony/browser-kit": "2.2.*",
|
||||
"symfony/css-selector": "2.2.*",
|
||||
"phpunit/phpunit": "3.7.*",
|
||||
"phpmd/phpmd": "1.4.*",
|
||||
"phploc/phploc": "1.7.*"
|
||||
},
|
||||
"minimum-stability": "dev",
|
||||
"autoload": {
|
||||
|
||||
1143
composer.lock
generated
1143
composer.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -1,12 +1,19 @@
|
||||
[git]
|
||||
client = '/usr/bin/git' ; Your git executable path
|
||||
repositories = '/var/www/projects/' ; Path to your repositories
|
||||
default_branch = 'master' ; Default branch when HEAD is detached
|
||||
repositories[] = '/home/git/repositories/' ; Path to your repositories
|
||||
; If you wish to add more repositories, just add a new line
|
||||
|
||||
; WINDOWS USERS
|
||||
;client = '"C:\Program Files (x86)\Git\bin\git.exe"' ; Your git executable path
|
||||
;repositories[] = 'C:\Path\to\Repos\' ; Path to your repositories
|
||||
|
||||
; You can hide repositories from GitList, just copy this for each repository you want to hide
|
||||
; hidden[] = '/var/www/projects/BetaTest'
|
||||
; hidden[] = '/home/git/repositories/BetaTest'
|
||||
|
||||
[app]
|
||||
debug = false
|
||||
cache = true
|
||||
|
||||
; If you need to specify custom filetypes for certain extensions, do this here
|
||||
[filetypes]
|
||||
|
||||
13
index.php
13
index.php
@@ -1,7 +1,7 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* GitList 0.3
|
||||
* GitList 0.4
|
||||
* https://github.com/klaussilveira/gitlist
|
||||
*/
|
||||
|
||||
@@ -10,10 +10,17 @@ if (!ini_get('date.timezone')) {
|
||||
date_default_timezone_set('UTC');
|
||||
}
|
||||
|
||||
if (php_sapi_name() == 'cli-server' && file_exists(substr($_SERVER['REQUEST_URI'], 1))) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!is_writable(__DIR__ . DIRECTORY_SEPARATOR . 'cache')) {
|
||||
die(sprintf('The "%s" folder must be writable for GitList to run.', __DIR__ . DIRECTORY_SEPARATOR . 'cache'));
|
||||
}
|
||||
|
||||
require 'vendor/autoload.php';
|
||||
|
||||
// Load configuration
|
||||
$config = GitList\Config::fromFile('config.ini');
|
||||
|
||||
$app = require 'boot.php';
|
||||
$app->run();
|
||||
|
||||
|
||||
147
pkg_builder/Makefile
Normal file
147
pkg_builder/Makefile
Normal file
@@ -0,0 +1,147 @@
|
||||
# Main Info
|
||||
NAME = gitlist
|
||||
DESCRIPTION = "An elegant and modern git repository viewer"
|
||||
LICENSE = New BSD
|
||||
GROUP = gitlist
|
||||
VENDOR = gitlist.org
|
||||
URL = "http://www.gitlist.org"
|
||||
|
||||
#BUILD Info
|
||||
PREFIX = /usr/share
|
||||
PROJROOT = "$(shell pwd)"
|
||||
SRCROOT = "$(shell pwd)/gitlist"
|
||||
UPSTREAM_VERSION = $(shell cat tools/release.info | head -n1 | cut -d"=" -f2)
|
||||
BUILD_STAMP = $(shell date +"%Y%m%d%H%M%S")
|
||||
|
||||
#Packager Info
|
||||
PACKAGER = $(shell git config user.name)
|
||||
PACKAGER_MAIL = $(shell git config user.email)
|
||||
|
||||
#Debian Package Info
|
||||
PACKAGE-VERSION= 1
|
||||
DEBIAN_BUILD_ROOT = ${PROJROOT}/debian/
|
||||
PROJECT_DEBIAN_LIKE_NAME=$(shell cat tools/release.info | grep name | cut -d"=" -f2)
|
||||
DEBIAN_NAME=$(PROJECT_DEBIAN_LIKE_NAME)$(shell echo "_")$(UPSTREAM_VERSION)-${PACKAGE-VERSION}$(shell echo "_all.deb")
|
||||
DEBIAN_VERSION =
|
||||
# Generating control file
|
||||
define control
|
||||
Package: $(PROJECT_DEBIAN_LIKE_NAME)
|
||||
Version: $(UPSTREAM_VERSION)-${PACKAGE-VERSION}
|
||||
Architecture: all
|
||||
Section: web
|
||||
Priority: optional
|
||||
Maintainer: "${PACKAGER} <${PACKAGER_MAIL}>"
|
||||
Description: ${DESCRIPTION}
|
||||
endef
|
||||
export control
|
||||
|
||||
all:
|
||||
@echo "... $(UPSTREAM_VERSION)"
|
||||
@echo "... $(PACKAGER)"
|
||||
@echo "... $(PACKAGER_MAIL)"
|
||||
@echo "... $(DEBIAN_NAME)"
|
||||
|
||||
help:
|
||||
@echo "To use this make file just:"
|
||||
@echo "Download the gitlist tarball and stract it into a folder called gitlist"
|
||||
@echo "make [build_deb|build_rpm|build(apache|nginx|lighthttp)]"
|
||||
|
||||
clean_deb:
|
||||
@echo "Cleaning . . ."
|
||||
@rm -rf ${DEBIAN_BUILD_ROOT}/*.deb
|
||||
@rm -rf ${PROJROOT}/debian
|
||||
|
||||
prepare_deb: clean_deb
|
||||
@echo "############################### - Building DEB"
|
||||
@mkdir ${DEBIAN_BUILD_ROOT} -pv
|
||||
@mkdir ${DEBIAN_BUILD_ROOT}/DEBIAN -pv
|
||||
@mkdir ${DEBIAN_BUILD_ROOT}${PREFIX}/${PROJECT_DEBIAN_LIKE_NAME} -pv
|
||||
|
||||
copy_deb_files: prepare_deb
|
||||
@echo "$$control" > ${DEBIAN_BUILD_ROOT}/DEBIAN/control
|
||||
|
||||
copy_deb: copy_deb_files
|
||||
@echo Sync files
|
||||
@rsync -avz ${SRCROOT} ${DEBIAN_BUILD_ROOT}${PREFIX}/
|
||||
|
||||
md5sum_deb: copy_deb
|
||||
@cd debian; find . -type f ! -regex '.*\.hg.*' ! -regex '.*?debian-binary.*' ! -regex '.*?DEBIAN.*' | xargs -d "\n" md5sum > DEBIAN/md5sums
|
||||
|
||||
deb_uniq: md5sum_deb
|
||||
@mkdir ${PROJROOT}/pkg -p
|
||||
@dpkg -b debian $(DEBIAN_NAME);
|
||||
@mv $(DEBIAN_NAME) ${PROJROOT}/pkg/
|
||||
@rm debian -rf
|
||||
@echo '### Wrote $(DEBIAN_NAME) in ${PROJROOT}/pkg/ . . . . . Success'
|
||||
|
||||
|
||||
build_deb: deb_uniq
|
||||
|
||||
#### RPM STUFF
|
||||
|
||||
RPM_NAME=$(PROJECT_DEBIAN_LIKE_NAME)$(shell echo "_")$(UPSTREAM_VERSION)-${PACKAGE-VERSION}$(shell echo "_all.rpm")
|
||||
|
||||
DIST_DIR = dist
|
||||
TAR_DIR = tar
|
||||
|
||||
RPM_DIR = rpm
|
||||
RPM_DIRS = SPECS RPMS SOURCES BUILD
|
||||
|
||||
clean_rpm:
|
||||
@echo Cleaning temporary dirs...
|
||||
@rm -rf $(TAR_DIR)
|
||||
@rm -rf $(RPM_DIR)
|
||||
@rm -rf $(DIST_DIR)
|
||||
|
||||
rpm_init: clean_rpm
|
||||
@echo Creating directories...
|
||||
@echo $(DIST_DIR)
|
||||
@mkdir -p $(DIST_DIR)
|
||||
@for dir in $(RPM_DIRS); do \
|
||||
echo $(RPM_DIR)/$$dir; \
|
||||
mkdir -p $(RPM_DIR)/$$dir; \
|
||||
done
|
||||
|
||||
rpm_preptar: rpm_init
|
||||
@echo Copying files to generate tar...
|
||||
@echo creating directory: $(TAR_DIR)/
|
||||
@mkdir $(TAR_DIR)/ -p
|
||||
@rsync -avz --exclude ".git" --exclude ".gitignore" --exclude "builder" gitlist $(TAR_DIR)/
|
||||
|
||||
rpm_tar: rpm_preptar
|
||||
@echo Generating tarball...
|
||||
@cd $(PROJROOT)/$(TAR_DIR); \
|
||||
tar cf $(PROJROOT)/$(RPM_DIR)/SOURCES/$(NAME).tar .
|
||||
|
||||
rpm: rpm_tar
|
||||
@echo Calling rpmbuild...
|
||||
@echo Vesion: $(VERSION)
|
||||
@cp tools/$(NAME).spec $(RPM_DIR)/SPECS/
|
||||
|
||||
@cd $(PROJROOT)/$(RPM_DIR)/SPECS ; \
|
||||
rpmbuild -bb \
|
||||
--buildroot="$(PROJROOT)/$(RPM_DIR)/BUILD/$(NAME)" \
|
||||
--define "_topdir $(PROJROOT)/$(RPM_DIR)" \
|
||||
--define "name $(NAME)" \
|
||||
--define "summary "$(DESCRIPTION)"" \
|
||||
--define "version $(UPSTREAM_VERSION)" \
|
||||
--define "release $(PACKAGE-VERSION)" \
|
||||
--define "url _$(URL)_" \
|
||||
--define "license $(LICENSE)" \
|
||||
--define "group $(GROUP)" \
|
||||
--define "vendor $(VENDOR)" \
|
||||
--define "packager $(PACKAGER)" \
|
||||
--define "prefix $(PREFIX)" \
|
||||
--define "source_dir $(PROJROOT)/$(RPM_DIR)/SOURCES" \
|
||||
$(NAME).spec
|
||||
@echo Copying generated RPM to dist dir...
|
||||
@mkdir ${PROJROOT}/pkg -p
|
||||
@cp $(PROJROOT)/$(RPM_DIR)/RPMS/noarch/*.rpm $(PROJROOT)/pkg
|
||||
@rm -rf $(TAR_DIR)
|
||||
@rm -rf $(RPM_DIR)
|
||||
@rm -rf $(DIST_DIR)
|
||||
|
||||
|
||||
|
||||
build_rpm: rpm
|
||||
|
||||
46
pkg_builder/README.md
Normal file
46
pkg_builder/README.md
Normal file
@@ -0,0 +1,46 @@
|
||||
# GitList Builder: Tools to build gitlist package
|
||||
|
||||
## Status
|
||||
|
||||
### Ready to build
|
||||
|
||||
* Core Deb Packages:
|
||||
```
|
||||
$ make build_deb
|
||||
```
|
||||
|
||||
* Core Rpm Packages:
|
||||
```
|
||||
$ make build_rpm
|
||||
```
|
||||
|
||||
### Not Ready (Comming soon)
|
||||
|
||||
Packages to install configuration files:
|
||||
|
||||
* make apache_deb
|
||||
* make apache_rpm
|
||||
* make nginx_deb
|
||||
* make nginx_rpm
|
||||
|
||||
## Dependencies
|
||||
|
||||
* To use this package builder you may need to install some development packages like: dpkg-dev or evem rpm;
|
||||
* A tarball of a stable release;
|
||||
|
||||
## Instructions
|
||||
|
||||
To use this builder just download the lastest stable release into this directory and build using the make functions avaible in make help
|
||||
|
||||
## How to build
|
||||
|
||||
The packages can be generated by running Makefile functions like:
|
||||
|
||||
```
|
||||
$ make build_deb
|
||||
$ make build_rpm
|
||||
```
|
||||
|
||||
## Structure
|
||||
|
||||
Core package is a simple source package while a configuration package requires all the dependencies.
|
||||
37
pkg_builder/tools/gitlist.spec
Normal file
37
pkg_builder/tools/gitlist.spec
Normal file
@@ -0,0 +1,37 @@
|
||||
Name: %{name}
|
||||
Summary: %{summary}
|
||||
Version: %{version}
|
||||
Release: %{release}
|
||||
URL: %{url}
|
||||
License: %{license}
|
||||
Group: %{group}
|
||||
Vendor: %{vendor}
|
||||
Packager: %{user}
|
||||
Prefix: %{prefix}
|
||||
BuildArch: noarch
|
||||
BuildRoot: %{_tmppath}/%{name}-%{version}-%{release}-root
|
||||
Source0: %{name}.tar
|
||||
|
||||
%description
|
||||
%{summary}
|
||||
|
||||
%prep
|
||||
%setup -c
|
||||
|
||||
%build
|
||||
|
||||
%install
|
||||
%{__rm} -rf %{buildroot}
|
||||
%{__mkdir} -p %{buildroot}%{prefix}
|
||||
%{__cp} -Ra * %{buildroot}%{prefix}
|
||||
|
||||
%clean
|
||||
rm -rf %{buildroot}
|
||||
|
||||
%files
|
||||
%defattr(0755,root,root)
|
||||
%{prefix}/*
|
||||
|
||||
%changelog
|
||||
* Thu Jan 21 2013 Bruno Gurgel <bruno.gurgel@gmail.com>
|
||||
- Initial
|
||||
2
pkg_builder/tools/release.info
Normal file
2
pkg_builder/tools/release.info
Normal file
@@ -0,0 +1,2 @@
|
||||
release=0.3
|
||||
name=gitlist
|
||||
@@ -15,6 +15,8 @@ use GitList\Provider\RoutingUtilServiceProvider;
|
||||
*/
|
||||
class Application extends SilexApplication
|
||||
{
|
||||
protected $path;
|
||||
|
||||
/**
|
||||
* Constructor initialize services.
|
||||
*
|
||||
@@ -24,30 +26,39 @@ class Application extends SilexApplication
|
||||
public function __construct(Config $config, $root = null)
|
||||
{
|
||||
parent::__construct();
|
||||
|
||||
$app = $this;
|
||||
$root = realpath($root);
|
||||
$this->path = realpath($root);
|
||||
|
||||
$this['debug'] = $config->get('app', 'debug');
|
||||
$this['filetypes'] = $config->getSection('filetypes');
|
||||
$this['cache.archives'] = $root . DIRECTORY_SEPARATOR . 'cache' . DIRECTORY_SEPARATOR . 'archives';
|
||||
$this['cache.archives'] = $this->getCachePath() . 'archives';
|
||||
|
||||
// Register services
|
||||
$this->register(new TwigServiceProvider(), array(
|
||||
'twig.path' => $root . DIRECTORY_SEPARATOR . 'views',
|
||||
'twig.options' => array('cache' => $root . DIRECTORY_SEPARATOR . 'cache' . DIRECTORY_SEPARATOR . 'views'),
|
||||
'twig.path' => $this->getViewPath(),
|
||||
'twig.options' => $config->get('app', 'cache') ?
|
||||
array('cache' => $this->getCachePath() . 'views') : array(),
|
||||
));
|
||||
|
||||
$repositories = $config->get('git', 'repositories');
|
||||
|
||||
$this->register(new GitServiceProvider(), array(
|
||||
'git.client' => $config->get('git', 'client'),
|
||||
'git.repos' => $config->get('git', 'repositories'),
|
||||
'git.hidden' => $config->get('git', 'hidden') ? $config->get('git', 'hidden') : array(),
|
||||
'git.client' => $config->get('git', 'client'),
|
||||
'git.repos' => $repositories,
|
||||
'ini.file' => "config.ini",
|
||||
'git.hidden' => $config->get('git', 'hidden') ?
|
||||
$config->get('git', 'hidden') : array(),
|
||||
'git.default_branch' => $config->get('git', 'default_branch') ?
|
||||
$config->get('git', 'default_branch') : 'master',
|
||||
));
|
||||
|
||||
$this->register(new ViewUtilServiceProvider());
|
||||
$this->register(new RepositoryUtilServiceProvider());
|
||||
$this->register(new UrlGeneratorServiceProvider());
|
||||
$this->register(new RoutingUtilServiceProvider());
|
||||
|
||||
$this['twig'] = $this->share($this->extend('twig', function($twig, $app) {
|
||||
$this['twig'] = $this->share($this->extend('twig', function ($twig, $app) {
|
||||
$twig->addFilter('htmlentities', new \Twig_Filter_Function('htmlentities'));
|
||||
$twig->addFilter('md5', new \Twig_Filter_Function('md5'));
|
||||
|
||||
return $twig;
|
||||
@@ -64,4 +75,25 @@ class Application extends SilexApplication
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
public function getPath()
|
||||
{
|
||||
return $this->path . DIRECTORY_SEPARATOR;
|
||||
}
|
||||
|
||||
public function setPath($path)
|
||||
{
|
||||
$this->path = $path;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getCachePath()
|
||||
{
|
||||
return $this->path . DIRECTORY_SEPARATOR . 'cache' . DIRECTORY_SEPARATOR;
|
||||
}
|
||||
|
||||
public function getViewPath()
|
||||
{
|
||||
return $this->path . DIRECTORY_SEPARATOR . 'views' . DIRECTORY_SEPARATOR;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,18 +6,22 @@ class Config
|
||||
{
|
||||
protected $data;
|
||||
|
||||
public static function fromFile($file) {
|
||||
public static function fromFile($file)
|
||||
{
|
||||
if (!file_exists($file)) {
|
||||
die(sprintf('Please, create the %1$s file.', $file));
|
||||
}
|
||||
|
||||
$data = parse_ini_file($file, true);
|
||||
return new static($data);
|
||||
$config = new static($data);
|
||||
$config->validateOptions();
|
||||
|
||||
return $config;
|
||||
}
|
||||
|
||||
public function __construct($data)
|
||||
public function __construct($data = array())
|
||||
{
|
||||
$this->data = $data;
|
||||
$this->validateOptions();
|
||||
}
|
||||
|
||||
public function get($section, $option)
|
||||
@@ -49,8 +53,26 @@ class Config
|
||||
|
||||
protected function validateOptions()
|
||||
{
|
||||
if (!$this->get('git', 'repositories') || !is_dir($this->get('git', 'repositories'))) {
|
||||
$repositories = $this->get('git', 'repositories');
|
||||
|
||||
$atLeastOneOk = false;
|
||||
$atLeastOneWrong = false;
|
||||
|
||||
foreach ($repositories as $directory) {
|
||||
if (!$directory || !is_dir($directory)) {
|
||||
$atLeastOneWrong = true;
|
||||
} else {
|
||||
$atLeastOneOk = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!$atLeastOneOk) {
|
||||
die("Please, edit the config file and provide your repositories directory");
|
||||
}
|
||||
|
||||
if ($atLeastOneWrong) {
|
||||
die("One or more of the supplied repository paths appears to be wrong. Please, check the config file");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -12,8 +12,12 @@ class BlobController implements ControllerProviderInterface
|
||||
{
|
||||
$route = $app['controllers_factory'];
|
||||
|
||||
$route->get('{repo}/blob/{branch}/{file}', function($repo, $branch, $file) use ($app) {
|
||||
$repository = $app['git']->getRepository($app['git.repos'] . $repo);
|
||||
$route->get('{repo}/blob/{commitishPath}', function ($repo, $commitishPath) use ($app) {
|
||||
$repository = $app['git']->getRepositoryFromName($app['git.repos'], $repo);
|
||||
|
||||
list($branch, $file) = $app['util.routing']
|
||||
->parseCommitishPathParam($commitishPath, $repo);
|
||||
|
||||
list($branch, $file) = $app['util.repository']->extractRef($repository, $branch, $file);
|
||||
|
||||
$blob = $repository->getBlob("$branch:\"$file\"");
|
||||
@@ -23,8 +27,7 @@ class BlobController implements ControllerProviderInterface
|
||||
if ($fileType !== 'image' && $app['util.repository']->isBinary($file)) {
|
||||
return $app->redirect($app['url_generator']->generate('blob_raw', array(
|
||||
'repo' => $repo,
|
||||
'branch' => $branch,
|
||||
'file' => $file,
|
||||
'commitishPath' => $commitishPath,
|
||||
)));
|
||||
}
|
||||
|
||||
@@ -38,31 +41,34 @@ class BlobController implements ControllerProviderInterface
|
||||
'branches' => $repository->getBranches(),
|
||||
'tags' => $repository->getTags(),
|
||||
));
|
||||
})->assert('file', '.+')
|
||||
->assert('repo', $app['util.routing']->getRepositoryRegex())
|
||||
->assert('branch', '[\w-._\/]+')
|
||||
})->assert('repo', $app['util.routing']->getRepositoryRegex())
|
||||
->assert('commitishPath', '.+')
|
||||
->bind('blob');
|
||||
|
||||
$route->get('{repo}/raw/{branch}/{file}', function($repo, $branch, $file) use ($app) {
|
||||
$repository = $app['git']->getRepository($app['git.repos'] . $repo);
|
||||
$route->get('{repo}/raw/{commitishPath}', function ($repo, $commitishPath) use ($app) {
|
||||
$repository = $app['git']->getRepositoryFromName($app['git.repos'], $repo);
|
||||
|
||||
list($branch, $file) = $app['util.routing']
|
||||
->parseCommitishPathParam($commitishPath, $repo);
|
||||
|
||||
list($branch, $file) = $app['util.repository']->extractRef($repository, $branch, $file);
|
||||
|
||||
$blob = $repository->getBlob("$branch:\"$file\"")->output();
|
||||
|
||||
$headers = array();
|
||||
if ($app['util.repository']->isBinary($file)) {
|
||||
$headers['Content-Disposition'] = 'attachment; filename="' . $file . '"';
|
||||
$headers['Content-Transfer-Encoding'] = 'application/octet-stream';
|
||||
$headers['Content-Transfer-Encoding'] = 'binary';
|
||||
$headers['Content-Type'] = 'application/octet-stream';
|
||||
} else {
|
||||
$headers['Content-Transfer-Encoding'] = 'text/plain';
|
||||
$headers['Content-Type'] = 'text/plain';
|
||||
}
|
||||
|
||||
return new Response($blob, 200, $headers);
|
||||
})->assert('file', '.+')
|
||||
->assert('repo', $app['util.routing']->getRepositoryRegex())
|
||||
->assert('branch', '[\w-._\/]+')
|
||||
})->assert('repo', $app['util.routing']->getRepositoryRegex())
|
||||
->assert('commitishPath', $app['util.routing']->getCommitishPathRegex())
|
||||
->bind('blob_raw');
|
||||
|
||||
return $route;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -12,14 +12,22 @@ class CommitController implements ControllerProviderInterface
|
||||
{
|
||||
$route = $app['controllers_factory'];
|
||||
|
||||
$route->get('{repo}/commits/{branch}/{file}', function($repo, $branch, $file) use ($app) {
|
||||
$repository = $app['git']->getRepository($app['git.repos'] . $repo);
|
||||
$route->get('{repo}/commits/{commitishPath}', function ($repo, $commitishPath) use ($app) {
|
||||
$repository = $app['git']->getRepositoryFromName($app['git.repos'], $repo);
|
||||
|
||||
if ($commitishPath === null) {
|
||||
$commitishPath = $repository->getHead();
|
||||
}
|
||||
|
||||
list($branch, $file) = $app['util.routing']
|
||||
->parseCommitishPathParam($commitishPath, $repo);
|
||||
|
||||
list($branch, $file) = $app['util.repository']->extractRef($repository, $branch, $file);
|
||||
|
||||
$type = $file ? "$branch -- \"$file\"" : $branch;
|
||||
$pager = $app['util.view']->getPager($app['request']->get('page'), $repository->getTotalCommits($type));
|
||||
$commits = $repository->getPaginatedCommits($type, $pager['current']);
|
||||
$categorized = array();
|
||||
|
||||
foreach ($commits as $commit) {
|
||||
$date = $commit->getDate();
|
||||
@@ -30,6 +38,7 @@ class CommitController implements ControllerProviderInterface
|
||||
$template = $app['request']->isXmlHttpRequest() ? 'commits_list.twig' : 'commits.twig';
|
||||
|
||||
return $app['twig']->render($template, array(
|
||||
'page' => 'commits',
|
||||
'pager' => $pager,
|
||||
'repo' => $repo,
|
||||
'branch' => $branch,
|
||||
@@ -39,15 +48,16 @@ class CommitController implements ControllerProviderInterface
|
||||
'file' => $file,
|
||||
));
|
||||
})->assert('repo', $app['util.routing']->getRepositoryRegex())
|
||||
->assert('branch', '[\w-._\/]+')
|
||||
->assert('file', '.+')
|
||||
->value('branch', 'master')
|
||||
->value('file', '')
|
||||
->assert('commitishPath', $app['util.routing']->getCommitishPathRegex())
|
||||
->value('commitishPath', null)
|
||||
->bind('commits');
|
||||
|
||||
$route->post('{repo}/commits/search', function(Request $request, $repo) use ($app) {
|
||||
$repository = $app['git']->getRepository($app['git.repos'] . $repo);
|
||||
$route->post('{repo}/commits/{branch}/search', function (Request $request, $repo, $branch = '') use ($app) {
|
||||
$repository = $app['git']->getRepositoryFromName($app['git.repos'], $repo);
|
||||
$query = $request->get('query');
|
||||
|
||||
$commits = $repository->searchCommitLog($request->get('query'));
|
||||
$categorized = array();
|
||||
|
||||
foreach ($commits as $commit) {
|
||||
$date = $commit->getDate();
|
||||
@@ -57,21 +67,24 @@ class CommitController implements ControllerProviderInterface
|
||||
|
||||
return $app['twig']->render('searchcommits.twig', array(
|
||||
'repo' => $repo,
|
||||
'branch' => 'master',
|
||||
'branch' => $branch,
|
||||
'file' => '',
|
||||
'commits' => $categorized,
|
||||
'branches' => $repository->getBranches(),
|
||||
'tags' => $repository->getTags(),
|
||||
'query' => $query
|
||||
));
|
||||
})->assert('repo', $app['util.routing']->getRepositoryRegex())
|
||||
->assert('branch', $app['util.routing']->getBranchRegex())
|
||||
->bind('searchcommits');
|
||||
|
||||
$route->get('{repo}/commit/{commit}/', function($repo, $commit) use ($app) {
|
||||
$repository = $app['git']->getRepository($app['git.repos'] . $repo);
|
||||
$route->get('{repo}/commit/{commit}', function ($repo, $commit) use ($app) {
|
||||
$repository = $app['git']->getRepositoryFromName($app['git.repos'], $repo);
|
||||
$commit = $repository->getCommit($commit);
|
||||
$branch = $repository->getHead();
|
||||
|
||||
return $app['twig']->render('commit.twig', array(
|
||||
'branch' => 'master',
|
||||
'branch' => $branch,
|
||||
'repo' => $repo,
|
||||
'commit' => $commit,
|
||||
));
|
||||
@@ -79,8 +92,11 @@ class CommitController implements ControllerProviderInterface
|
||||
->assert('commit', '[a-f0-9^]+')
|
||||
->bind('commit');
|
||||
|
||||
$route->get('{repo}/blame/{branch}/{file}', function($repo, $branch, $file) use ($app) {
|
||||
$repository = $app['git']->getRepository($app['git.repos'] . $repo);
|
||||
$route->get('{repo}/blame/{commitishPath}', function ($repo, $commitishPath) use ($app) {
|
||||
$repository = $app['git']->getRepositoryFromName($app['git.repos'], $repo);
|
||||
|
||||
list($branch, $file) = $app['util.routing']
|
||||
->parseCommitishPathParam($commitishPath, $repo);
|
||||
|
||||
list($branch, $file) = $app['util.repository']->extractRef($repository, $branch, $file);
|
||||
|
||||
@@ -95,10 +111,10 @@ class CommitController implements ControllerProviderInterface
|
||||
'blames' => $blames,
|
||||
));
|
||||
})->assert('repo', $app['util.routing']->getRepositoryRegex())
|
||||
->assert('file', '.+')
|
||||
->assert('branch', '[\w-._\/]+')
|
||||
->assert('commitishPath', $app['util.routing']->getCommitishPathRegex())
|
||||
->bind('blame');
|
||||
|
||||
return $route;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ namespace GitList\Controller;
|
||||
use Silex\Application;
|
||||
use Silex\ControllerProviderInterface;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
|
||||
class MainController implements ControllerProviderInterface
|
||||
{
|
||||
@@ -13,23 +14,28 @@ class MainController implements ControllerProviderInterface
|
||||
$route = $app['controllers_factory'];
|
||||
|
||||
$route->get('/', function() use ($app) {
|
||||
$repositories = array_map(
|
||||
function ($repo) use ($app) {
|
||||
$repo['relativePath'] = $app['util.routing']->getRelativePath($repo['path']);
|
||||
return $repo;
|
||||
},
|
||||
$app['git']->getRepositories($app['git.repos'])
|
||||
);
|
||||
$repositories = $app['git']->getRepositories($app['git.repos']);
|
||||
|
||||
return $app['twig']->render('index.twig', array(
|
||||
'repositories' => $repositories,
|
||||
));
|
||||
})->bind('homepage');
|
||||
|
||||
|
||||
$route->get('/refresh', function(Request $request) use ($app ) {
|
||||
# Go back to calling page
|
||||
return $app->redirect($request->headers->get('Referer'));
|
||||
})->bind('refresh');
|
||||
|
||||
$route->get('{repo}/stats/{branch}', function($repo, $branch) use ($app) {
|
||||
$repository = $app['git']->getRepository($app['git.repos'] . $repo);
|
||||
$repository = $app['git']->getRepositoryFromName($app['git.repos'], $repo);
|
||||
|
||||
if ($branch === null) {
|
||||
$branch = $repository->getHead();
|
||||
}
|
||||
|
||||
$stats = $repository->getStatistics($branch);
|
||||
$authors = $repository->getAuthorStatistics();
|
||||
$authors = $repository->getAuthorStatistics($branch);
|
||||
|
||||
return $app['twig']->render('stats.twig', array(
|
||||
'repo' => $repo,
|
||||
@@ -37,15 +43,20 @@ class MainController implements ControllerProviderInterface
|
||||
'branches' => $repository->getBranches(),
|
||||
'tags' => $repository->getTags(),
|
||||
'stats' => $stats,
|
||||
'authors' => $authors,
|
||||
'authors' => $authors,
|
||||
));
|
||||
})->assert('repo', $app['util.routing']->getRepositoryRegex())
|
||||
->assert('branch', '[\w-._\/]+')
|
||||
->value('branch', 'master')
|
||||
->assert('branch', $app['util.routing']->getBranchRegex())
|
||||
->value('branch', null)
|
||||
->bind('stats');
|
||||
|
||||
$route->get('{repo}/{branch}/rss/', function($repo, $branch) use ($app) {
|
||||
$repository = $app['git']->getRepository($app['git.repos'] . $repo);
|
||||
$repository = $app['git']->getRepositoryFromName($app['git.repos'], $repo);
|
||||
|
||||
if ($branch === null) {
|
||||
$branch = $repository->getHead();
|
||||
}
|
||||
|
||||
$commits = $repository->getPaginatedCommits($branch);
|
||||
|
||||
$html = $app['twig']->render('rss.twig', array(
|
||||
@@ -56,7 +67,8 @@ class MainController implements ControllerProviderInterface
|
||||
|
||||
return new Response($html, 200, array('Content-Type' => 'application/rss+xml'));
|
||||
})->assert('repo', $app['util.routing']->getRepositoryRegex())
|
||||
->assert('branch', '[\w-._\/]+')
|
||||
->assert('branch', $app['util.routing']->getBranchRegex())
|
||||
->value('branch', null)
|
||||
->bind('rss');
|
||||
|
||||
return $route;
|
||||
|
||||
113
src/GitList/Controller/NetworkController.php
Normal file
113
src/GitList/Controller/NetworkController.php
Normal file
@@ -0,0 +1,113 @@
|
||||
<?php
|
||||
|
||||
namespace GitList\Controller;
|
||||
|
||||
use GitList\Git\Repository;
|
||||
use Gitter\Model\Commit\Commit;
|
||||
use Silex\Application;
|
||||
use Silex\ControllerProviderInterface;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
|
||||
class NetworkController implements ControllerProviderInterface
|
||||
{
|
||||
public function connect(Application $app)
|
||||
{
|
||||
$route = $app['controllers_factory'];
|
||||
|
||||
$route->get('{repo}/network/{commitishPath}/{page}.json',
|
||||
function ($repo, $commitishPath, $page) use ($app) {
|
||||
/** @var $repository Repository */
|
||||
$repository = $app['git']->getRepositoryFromName($app['git.repos'], $repo);
|
||||
|
||||
if ($commitishPath === null) {
|
||||
$commitishPath = $repository->getHead();
|
||||
}
|
||||
|
||||
$pager = $app['util.view']->getPager($page, $repository->getTotalCommits($commitishPath));
|
||||
$commits = $repository->getPaginatedCommits($commitishPath, $pager['current']);
|
||||
|
||||
$jsonFormattedCommits = array();
|
||||
|
||||
foreach ($commits as $commit) {
|
||||
$detailsUrl = $app['url_generator']->generate(
|
||||
'commit',
|
||||
array(
|
||||
'repo' => $repo,
|
||||
'commit' => $commit->getHash()
|
||||
)
|
||||
);
|
||||
|
||||
$jsonFormattedCommits[$commit->getHash()] = array(
|
||||
'hash' => $commit->getHash(),
|
||||
'parentsHash' => $commit->getParentsHash(),
|
||||
'date' => $commit->getDate()->format('U'),
|
||||
'message' => htmlentities($commit->getMessage()),
|
||||
'details' => $detailsUrl,
|
||||
'author' => array(
|
||||
'name' => $commit->getAuthor()->getName(),
|
||||
'email' => $commit->getAuthor()->getEmail(),
|
||||
// due to the lack of a inbuilt javascript md5 mechanism, build the full avatar url on the php side
|
||||
'image' => 'http://gravatar.com/avatar/' . md5(
|
||||
strtolower($commit->getAuthor()->getEmail())
|
||||
) . '?s=40'
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
$nextPageUrl = null;
|
||||
if ($pager['last'] !== $pager['current']) {
|
||||
$nextPageUrl = $app['url_generator']->generate(
|
||||
'networkData',
|
||||
array(
|
||||
'repo' => $repo,
|
||||
'commitishPath' => $commitishPath,
|
||||
'page' => $pager['next']
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
return $app->json( array(
|
||||
'repo' => $repo,
|
||||
'commitishPath' => $commitishPath,
|
||||
'nextPage' => $nextPageUrl,
|
||||
'start' => $commits[0]->getHash(),
|
||||
'commits' => $jsonFormattedCommits
|
||||
), 200
|
||||
);
|
||||
}
|
||||
)->assert('repo', $app['util.routing']->getRepositoryRegex())
|
||||
->assert('commitishPath', $app['util.routing']->getCommitishPathRegex())
|
||||
->value('commitishPath', null)
|
||||
->assert('page', '\d+')
|
||||
->value('page', '0')
|
||||
->bind('networkData');
|
||||
|
||||
$route->get(
|
||||
'{repo}/network/{commitishPath}',
|
||||
function ($repo, $commitishPath) use ($app) {
|
||||
$repository = $app['git']->getRepositoryFromName($app['git.repos'], $repo);
|
||||
|
||||
if ($commitishPath === null) {
|
||||
$commitishPath = $repository->getHead();
|
||||
}
|
||||
|
||||
list($branch, $file) = $app['util.routing']->parseCommitishPathParam($commitishPath, $repo);
|
||||
list($branch, $file) = $app['util.repository']->extractRef($repository, $branch, $file);
|
||||
|
||||
return $app['twig']->render(
|
||||
'network.twig',
|
||||
array(
|
||||
'repo' => $repo,
|
||||
'branch' => $branch,
|
||||
'commitishPath' => $commitishPath,
|
||||
)
|
||||
);
|
||||
}
|
||||
)->assert('repo', $app['util.routing']->getRepositoryRegex())
|
||||
->assert('commitishPath', $app['util.routing']->getCommitishPathRegex())
|
||||
->value('commitishPath', null)
|
||||
->bind('network');
|
||||
|
||||
return $route;
|
||||
}
|
||||
}
|
||||
@@ -13,12 +13,14 @@ class TreeController implements ControllerProviderInterface
|
||||
{
|
||||
$route = $app['controllers_factory'];
|
||||
|
||||
$route->get('{repo}/tree/{branch}/{tree}/', $treeController = function($repo, $branch = '', $tree = '') use ($app) {
|
||||
$repository = $app['git']->getRepository($app['git.repos'] . $repo);
|
||||
if (!$branch) {
|
||||
$branch = $repository->getHead();
|
||||
$route->get('{repo}/tree/{commitishPath}/', $treeController = function ($repo, $commitishPath = '') use ($app) {
|
||||
$repository = $app['git']->getRepositoryFromName($app['git.repos'], $repo);
|
||||
if (!$commitishPath) {
|
||||
$commitishPath = $repository->getHead();
|
||||
}
|
||||
|
||||
list($branch, $tree) = $app['util.routing']->parseCommitishPathParam($commitishPath, $repo);
|
||||
|
||||
list($branch, $tree) = $app['util.repository']->extractRef($repository, $branch, $tree);
|
||||
$files = $repository->getTree($tree ? "$branch:\"$tree\"/" : $branch);
|
||||
$breadcrumbs = $app['util.view']->getBreadcrumbs($tree);
|
||||
@@ -39,22 +41,21 @@ class TreeController implements ControllerProviderInterface
|
||||
'breadcrumbs' => $breadcrumbs,
|
||||
'branches' => $repository->getBranches(),
|
||||
'tags' => $repository->getTags(),
|
||||
'readme' => $app['util.repository']->getReadme($repo, $branch),
|
||||
'readme' => $app['util.repository']->getReadme($repository, $branch),
|
||||
));
|
||||
})->assert('repo', $app['util.routing']->getRepositoryRegex())
|
||||
->assert('branch', '[\w-._\/]+')
|
||||
->assert('tree', '.+')
|
||||
->assert('commitishPath', $app['util.routing']->getCommitishPathRegex())
|
||||
->bind('tree');
|
||||
|
||||
$route->post('{repo}/tree/{branch}/search', function(Request $request, $repo, $branch = '', $tree = '') use ($app) {
|
||||
$repository = $app['git']->getRepository($app['git.repos'] . $repo);
|
||||
|
||||
$route->post('{repo}/tree/{branch}/search', function (Request $request, $repo, $branch = '', $tree = '') use ($app) {
|
||||
$repository = $app['git']->getRepositoryFromName($app['git.repos'], $repo);
|
||||
if (!$branch) {
|
||||
$branch = $repository->getHead();
|
||||
}
|
||||
|
||||
$breadcrumbs = $app['util.view']->getBreadcrumbs($tree);
|
||||
$results = $repository->searchTree($request->get('query'), $branch);
|
||||
$query = $request->get('query');
|
||||
$breadcrumbs = array(array('dir' => 'Search results for: ' . $query, 'path' => ''));
|
||||
$results = $repository->searchTree($query, $branch);
|
||||
|
||||
return $app['twig']->render('search.twig', array(
|
||||
'results' => $results,
|
||||
@@ -66,22 +67,15 @@ class TreeController implements ControllerProviderInterface
|
||||
'tags' => $repository->getTags(),
|
||||
));
|
||||
})->assert('repo', $app['util.routing']->getRepositoryRegex())
|
||||
->assert('branch', '[\w-._\/]+')
|
||||
->assert('branch', $app['util.routing']->getBranchRegex())
|
||||
->bind('search');
|
||||
|
||||
$route->get('{repo}/{branch}/', function($repo, $branch) use ($app, $treeController) {
|
||||
return $treeController($repo, $branch);
|
||||
})->assert('repo', $app['util.routing']->getRepositoryRegex())
|
||||
->assert('branch', '[\w-._\/]+')
|
||||
->bind('branch');
|
||||
|
||||
$route->get('{repo}/', function($repo) use ($app, $treeController) {
|
||||
return $treeController($repo);
|
||||
})->assert('repo', $app['util.routing']->getRepositoryRegex())
|
||||
->bind('repository');
|
||||
|
||||
# Intentionally before next statement, because order appears
|
||||
# to be important, and the other statement got precedence previously.
|
||||
$route->get('{repo}/{format}ball/{branch}', function($repo, $format, $branch) use ($app) {
|
||||
$repository = $app['git']->getRepository($app['git.repos'] . $repo);
|
||||
$repository = $app['git']->getRepositoryFromName($app['git.repos'], $repo);
|
||||
|
||||
$tree = $repository->getBranchTree($branch);
|
||||
|
||||
if (false === $tree) {
|
||||
@@ -109,9 +103,22 @@ class TreeController implements ControllerProviderInterface
|
||||
));
|
||||
})->assert('format', '(zip|tar)')
|
||||
->assert('repo', $app['util.routing']->getRepositoryRegex())
|
||||
->assert('branch', '[\w-._\/]+')
|
||||
->assert('branch', $app['util.routing']->getBranchRegex())
|
||||
->bind('archive');
|
||||
|
||||
|
||||
$route->get('{repo}/{branch}/', function($repo, $branch) use ($app, $treeController) {
|
||||
return $treeController($repo, $branch);
|
||||
})->assert('repo', $app['util.routing']->getRepositoryRegex())
|
||||
->assert('branch', $app['util.routing']->getBranchRegex())
|
||||
->bind('branch');
|
||||
|
||||
$route->get('{repo}/', function($repo) use ($app, $treeController) {
|
||||
return $treeController($repo);
|
||||
})->assert('repo', $app['util.routing']->getRepositoryRegex())
|
||||
->bind('repository');
|
||||
|
||||
return $route;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,4 +2,7 @@
|
||||
|
||||
namespace GitList\Exception;
|
||||
|
||||
class BlankDataException extends \RuntimeException {}
|
||||
class BlankDataException extends \RuntimeException
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
8
src/GitList/Exception/EmptyRepositoryException.php
Normal file
8
src/GitList/Exception/EmptyRepositoryException.php
Normal file
@@ -0,0 +1,8 @@
|
||||
<?php
|
||||
|
||||
namespace GitList\Exception;
|
||||
|
||||
class EmptyRepositoryException extends \RuntimeException
|
||||
{
|
||||
|
||||
}
|
||||
@@ -6,11 +6,158 @@ use Gitter\Client as BaseClient;
|
||||
|
||||
class Client extends BaseClient
|
||||
{
|
||||
protected $defaultBranch;
|
||||
protected $hidden;
|
||||
|
||||
public function __construct($options = null)
|
||||
{
|
||||
parent::__construct($options['path']);
|
||||
$this->setDefaultBranch($options['default_branch']);
|
||||
$this->setHidden($options['hidden']);
|
||||
}
|
||||
|
||||
public function getRepositoryFromName($paths, $repo)
|
||||
{
|
||||
$repositories = $this->getRepositories($paths);
|
||||
$path = $repositories[$repo]['path'];
|
||||
|
||||
return $this->getRepository($path);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new repository on the specified path
|
||||
* Searches for valid repositories on the specified path
|
||||
*
|
||||
* @param string $path Path where the new repository will be created
|
||||
* @return Repository Instance of Repository
|
||||
* @param array $paths Array of paths where repositories will be searched
|
||||
* @return array Found repositories, containing their name, path and description
|
||||
*/
|
||||
public function getRepositories($paths)
|
||||
{
|
||||
$allRepositories = array();
|
||||
|
||||
foreach ($paths as $path) {
|
||||
$repositories = $this->recurseDirectory($path);
|
||||
|
||||
if (empty($repositories)) {
|
||||
throw new \RuntimeException('There are no GIT repositories in ' . $path);
|
||||
}
|
||||
|
||||
$allRepositories = array_merge($allRepositories, $repositories);
|
||||
}
|
||||
|
||||
$allRepositories = array_unique($allRepositories, SORT_REGULAR);
|
||||
asort($allRepositories);
|
||||
|
||||
return $allRepositories;
|
||||
}
|
||||
|
||||
private function recurseDirectory($path, $topLevel = true)
|
||||
{
|
||||
$dir = new \DirectoryIterator($path);
|
||||
|
||||
$repositories = array();
|
||||
|
||||
foreach ($dir as $file) {
|
||||
if ($file->isDot()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (strrpos($file->getFilename(), '.') === 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!$file->isReadable()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($file->isDir()) {
|
||||
$isBare = file_exists($file->getPathname() . '/HEAD');
|
||||
$isRepository = file_exists($file->getPathname() . '/.git/HEAD');
|
||||
|
||||
if ($isRepository || $isBare) {
|
||||
if (in_array($file->getPathname(), $this->getHidden())) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($isBare) {
|
||||
$description = $file->getPathname() . '/description';
|
||||
} else {
|
||||
$description = $file->getPathname() . '/.git/description';
|
||||
}
|
||||
|
||||
if (file_exists($description)) {
|
||||
$description = file_get_contents($description);
|
||||
} else {
|
||||
$description = null;
|
||||
}
|
||||
|
||||
if (!$topLevel) {
|
||||
$repoName = $file->getPathInfo()->getFilename() . '/' . $file->getFilename();
|
||||
} else {
|
||||
$repoName = $file->getFilename();
|
||||
}
|
||||
|
||||
$repositories[$repoName] = array(
|
||||
'name' => $repoName,
|
||||
'path' => $file->getPathname(),
|
||||
'description' => $description
|
||||
);
|
||||
|
||||
continue;
|
||||
} else {
|
||||
$repositories = array_merge($repositories, $this->recurseDirectory($file->getPathname(), false));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $repositories;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set default branch as a string.
|
||||
*
|
||||
* @param string $branch Name of branch to use when repo's HEAD is detached.
|
||||
*/
|
||||
protected function setDefaultBranch($branch)
|
||||
{
|
||||
$this->defaultBranch = $branch;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return name of default branch as a string.
|
||||
*/
|
||||
public function getDefaultBranch()
|
||||
{
|
||||
return $this->defaultBranch;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get hidden repository list
|
||||
*
|
||||
* @return array List of repositories to hide
|
||||
*/
|
||||
protected function getHidden()
|
||||
{
|
||||
return $this->hidden;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the hidden repository list
|
||||
*
|
||||
* @param array $hidden List of repositories to hide
|
||||
*/
|
||||
protected function setHidden($hidden)
|
||||
{
|
||||
$this->hidden = $hidden;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Overloads the parent::createRepository method for the correct Repository class instance
|
||||
*
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function createRepository($path, $bare = null)
|
||||
{
|
||||
@@ -24,10 +171,9 @@ class Client extends BaseClient
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens a repository at the specified path
|
||||
*
|
||||
* @param string $path Path where the repository is located
|
||||
* @return Repository Instance of Repository
|
||||
* Overloads the parent::getRepository method for the correct Repository class instance
|
||||
*
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getRepository($path)
|
||||
{
|
||||
@@ -35,10 +181,7 @@ class Client extends BaseClient
|
||||
throw new \RuntimeException('There is no GIT repository at ' . $path);
|
||||
}
|
||||
|
||||
if (in_array($path, $this->getHidden())) {
|
||||
throw new \RuntimeException('You don\'t have access to this repository');
|
||||
}
|
||||
|
||||
return new Repository($path, $this);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -4,10 +4,202 @@ namespace GitList\Git;
|
||||
|
||||
use Gitter\Repository as BaseRepository;
|
||||
use Gitter\Model\Commit\Commit;
|
||||
use Gitter\Model\Commit\Diff;
|
||||
use Gitter\PrettyFormat;
|
||||
use Symfony\Component\Filesystem\Filesystem;
|
||||
|
||||
class Repository extends BaseRepository
|
||||
{
|
||||
/**
|
||||
* Return true if the repo contains this commit.
|
||||
*
|
||||
* @param $commitHash Hash of commit whose existence we want to check
|
||||
* @return boolean Whether or not the commit exists in this repo
|
||||
*/
|
||||
public function hasCommit($commitHash)
|
||||
{
|
||||
$logs = $this->getClient()->run($this, "show $commitHash");
|
||||
$logs = explode("\n", $logs);
|
||||
|
||||
return strpos($logs[0], 'commit') === 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current branch, returning a default value when HEAD is detached.
|
||||
*/
|
||||
public function getHead($default = null)
|
||||
{
|
||||
$client = $this->getClient();
|
||||
|
||||
return parent::getHead($client->getDefaultBranch());
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the data from a specific commit
|
||||
*
|
||||
* @param string $commitHash Hash of the specific commit to read data
|
||||
* @return array Commit data
|
||||
*/
|
||||
public function getCommit($commitHash)
|
||||
{
|
||||
$logs = $this->getClient()->run($this,
|
||||
"show --pretty=format:\"<item><hash>%H</hash>"
|
||||
. "<short_hash>%h</short_hash><tree>%T</tree><parents>%P</parents>"
|
||||
. "<author>%an</author><author_email>%ae</author_email>"
|
||||
. "<date>%at</date><commiter>%cn</commiter><commiter_email>%ce</commiter_email>"
|
||||
. "<commiter_date>%ct</commiter_date>"
|
||||
. "<message><![CDATA[%s]]></message>"
|
||||
. "<body><![CDATA[%b]]></body>"
|
||||
. "</item>\" $commitHash"
|
||||
);
|
||||
|
||||
$xmlEnd = strpos($logs, '</item>') + 7;
|
||||
$commitInfo = substr($logs, 0, $xmlEnd);
|
||||
$commitData = substr($logs, $xmlEnd);
|
||||
$logs = explode("\n", $commitData);
|
||||
|
||||
// Read commit metadata
|
||||
$format = new PrettyFormat;
|
||||
$data = $format->parse($commitInfo);
|
||||
$commit = new Commit;
|
||||
$commit->importData($data[0]);
|
||||
|
||||
if ($commit->getParentsHash()) {
|
||||
$command = 'diff ' . $commitHash . '~1..' . $commitHash;
|
||||
$logs = explode("\n", $this->getClient()->run($this, $command));
|
||||
}
|
||||
|
||||
$commit->setDiffs($this->readDiffLogs($logs));
|
||||
|
||||
return $commit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Blames the provided file and parses the output
|
||||
*
|
||||
* @param string $file File that will be blamed
|
||||
* @return array Commits hashes containing the lines
|
||||
*/
|
||||
public function getBlame($file)
|
||||
{
|
||||
$blame = array();
|
||||
$logs = $this->getClient()->run($this, "blame --root -sl $file");
|
||||
$logs = explode("\n", $logs);
|
||||
|
||||
$i = 0;
|
||||
$previousCommit = '';
|
||||
foreach ($logs as $log) {
|
||||
if ($log == '') {
|
||||
continue;
|
||||
}
|
||||
|
||||
preg_match_all("/([a-zA-Z0-9]{40})\s+.*?([0-9]+)\)(.+)/", $log, $match);
|
||||
|
||||
$currentCommit = $match[1][0];
|
||||
if ($currentCommit != $previousCommit) {
|
||||
++$i;
|
||||
$blame[$i] = array(
|
||||
'line' => '',
|
||||
'commit' => $currentCommit,
|
||||
'commitShort' => substr($currentCommit, 0, 8)
|
||||
);
|
||||
}
|
||||
|
||||
$blame[$i]['line'] .= PHP_EOL . $match[3][0];
|
||||
$previousCommit = $currentCommit;
|
||||
}
|
||||
|
||||
return $blame;
|
||||
}
|
||||
|
||||
/**
|
||||
* Read diff logs and generate a collection of diffs
|
||||
*
|
||||
* @param array $logs Array of log rows
|
||||
* @return array Array of diffs
|
||||
*/
|
||||
public function readDiffLogs(array $logs)
|
||||
{
|
||||
$diffs = array();
|
||||
$lineNumOld = 0;
|
||||
$lineNumNew = 0;
|
||||
foreach ($logs as $log) {
|
||||
# Skip empty lines
|
||||
if ($log == "") {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ('diff' === substr($log, 0, 4)) {
|
||||
if (isset($diff)) {
|
||||
$diffs[] = $diff;
|
||||
}
|
||||
|
||||
$diff = new Diff;
|
||||
if (preg_match('/^diff --[\S]+ a\/?(.+) b\/?/', $log, $name)) {
|
||||
$diff->setFile($name[1]);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if ('index' === substr($log, 0, 5)) {
|
||||
$diff->setIndex($log);
|
||||
continue;
|
||||
}
|
||||
|
||||
if ('---' === substr($log, 0, 3)) {
|
||||
$diff->setOld($log);
|
||||
continue;
|
||||
}
|
||||
|
||||
if ('+++' === substr($log, 0, 3)) {
|
||||
$diff->setNew($log);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Handle binary files properly.
|
||||
if ('Binary' === substr($log, 0, 6)) {
|
||||
$m = array();
|
||||
if (preg_match('/Binary files (.+) and (.+) differ/', $log, $m)) {
|
||||
$diff->setOld($m[1]);
|
||||
$diff->setNew(" {$m[2]}");
|
||||
}
|
||||
}
|
||||
|
||||
if (!empty($log)) {
|
||||
switch ($log[0]) {
|
||||
case "@":
|
||||
// Set the line numbers
|
||||
preg_match('/@@ -([0-9]+)/', $log, $matches);
|
||||
$lineNumOld = $matches[1] - 1;
|
||||
$lineNumNew = $matches[1] - 1;
|
||||
break;
|
||||
case "-":
|
||||
$lineNumOld++;
|
||||
break;
|
||||
case "+":
|
||||
$lineNumNew++;
|
||||
break;
|
||||
default:
|
||||
$lineNumOld++;
|
||||
$lineNumNew++;
|
||||
}
|
||||
} else {
|
||||
$lineNumOld++;
|
||||
$lineNumNew++;
|
||||
}
|
||||
|
||||
if (isset($diff)) {
|
||||
$diff->addLine($log, $lineNumOld, $lineNumNew);
|
||||
}
|
||||
}
|
||||
|
||||
if (isset($diff)) {
|
||||
$diffs[] = $diff;
|
||||
}
|
||||
|
||||
return $diffs;
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the repository commit log with pagination
|
||||
*
|
||||
@@ -18,13 +210,24 @@ class Repository extends BaseRepository
|
||||
{
|
||||
$page = 15 * $page;
|
||||
$pager = "--skip=$page --max-count=15";
|
||||
$command = "log $pager --pretty=format:'<item><hash>%H</hash><short_hash>%h</short_hash><tree>%T</tree><parent>%P</parent><author>%an</author><author_email>%ae</author_email><date>%at</date><commiter>%cn</commiter><commiter_email>%ce</commiter_email><commiter_date>%ct</commiter_date><message><![CDATA[%s]]></message></item>'";
|
||||
$command =
|
||||
"log $pager --pretty=format:\"<item><hash>%H</hash>"
|
||||
. "<short_hash>%h</short_hash><tree>%T</tree><parents>%P</parents>"
|
||||
. "<author>%an</author><author_email>%ae</author_email>"
|
||||
. "<date>%at</date><commiter>%cn</commiter>"
|
||||
. "<commiter_email>%ce</commiter_email>"
|
||||
. "<commiter_date>%ct</commiter_date>"
|
||||
. "<message><![CDATA[%s]]></message></item>\"";
|
||||
|
||||
if ($file) {
|
||||
$command .= " $file";
|
||||
}
|
||||
|
||||
$logs = $this->getPrettyFormat($command);
|
||||
try {
|
||||
$logs = $this->getPrettyFormat($command);
|
||||
} catch (\RuntimeException $e) {
|
||||
return array();
|
||||
}
|
||||
|
||||
foreach ($logs as $log) {
|
||||
$commit = new Commit;
|
||||
@@ -37,9 +240,21 @@ class Repository extends BaseRepository
|
||||
|
||||
public function searchCommitLog($query)
|
||||
{
|
||||
$command = "log --grep='$query' --pretty=format:'<item><hash>%H</hash><short_hash>%h</short_hash><tree>%T</tree><parent>%P</parent><author>%an</author><author_email>%ae</author_email><date>%at</date><commiter>%cn</commiter><commiter_email>%ce</commiter_email><commiter_date>%ct</commiter_date><message><![CDATA[%s]]></message></item>'";
|
||||
$query = escapeshellarg($query);
|
||||
$command =
|
||||
"log --grep={$query} --pretty=format:\"<item><hash>%H</hash>"
|
||||
. "<short_hash>%h</short_hash><tree>%T</tree><parents>%P</parents>"
|
||||
. "<author>%an</author><author_email>%ae</author_email>"
|
||||
. "<date>%at</date><commiter>%cn</commiter>"
|
||||
. "<commiter_email>%ce</commiter_email>"
|
||||
. "<commiter_date>%ct</commiter_date>"
|
||||
. "<message><![CDATA[%s]]></message></item>\"";
|
||||
|
||||
$logs = $this->getPrettyFormat($command);
|
||||
try {
|
||||
$logs = $this->getPrettyFormat($command);
|
||||
} catch (\RuntimeException $e) {
|
||||
return array();
|
||||
}
|
||||
|
||||
foreach ($logs as $log) {
|
||||
$commit = new Commit;
|
||||
@@ -52,8 +267,10 @@ class Repository extends BaseRepository
|
||||
|
||||
public function searchTree($query, $branch)
|
||||
{
|
||||
$query = escapeshellarg($query);
|
||||
|
||||
try {
|
||||
$results = $this->getClient()->run($this, "grep -I --line-number '$query' $branch");
|
||||
$results = $this->getClient()->run($this, "grep -I --line-number {$query} $branch");
|
||||
} catch (\RuntimeException $e) {
|
||||
return false;
|
||||
}
|
||||
@@ -65,20 +282,22 @@ class Repository extends BaseRepository
|
||||
continue;
|
||||
}
|
||||
|
||||
preg_match_all('/([\w-._]+):(.+):([0-9]+):(.+)/', $result, $matches, PREG_SET_ORDER);
|
||||
preg_match_all('/([\w-._]+):([^:]+):([0-9]+):(.+)/', $result, $matches, PREG_SET_ORDER);
|
||||
|
||||
$data['branch'] = $matches[0][1];
|
||||
$data['file'] = $matches[0][2];
|
||||
$data['line'] = $matches[0][3];
|
||||
$data['match'] = $matches[0][4];
|
||||
$data['file'] = $matches[0][2];
|
||||
$data['line'] = $matches[0][3];
|
||||
$data['match'] = $matches[0][4];
|
||||
|
||||
$searchResults[] = $data;
|
||||
}
|
||||
|
||||
return $searchResults;
|
||||
}
|
||||
|
||||
public function getAuthorStatistics()
|
||||
public function getAuthorStatistics($branch)
|
||||
{
|
||||
$logs = $this->getClient()->run($this, 'log --pretty=format:"%an||%ae" ' . $this->getHead());
|
||||
$logs = $this->getClient()->run($this, 'log --pretty=format:"%an||%ae" ' . $branch);
|
||||
|
||||
if (empty($logs)) {
|
||||
throw new \RuntimeException('No statistics available');
|
||||
@@ -124,8 +343,12 @@ class Repository extends BaseRepository
|
||||
$data['size'] += $file[3];
|
||||
}
|
||||
|
||||
if (($pos = strrpos($file[4], '.')) !== FALSE) {
|
||||
$data['extensions'][] = substr($file[4], $pos);
|
||||
if (($pos = strrpos($file[4], '.')) !== false) {
|
||||
$extension = substr($file[4], $pos);
|
||||
|
||||
if (($pos = strrpos($extension, '/')) === false) {
|
||||
$data['extensions'][] = $extension;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -148,4 +371,25 @@ class Repository extends BaseRepository
|
||||
$fs->mkdir(dirname($output));
|
||||
$this->getClient()->run($this, "archive --format=$format --output=$output $tree");
|
||||
}
|
||||
|
||||
/**
|
||||
* Return true if $path exists in $branch; return false otherwise.
|
||||
*
|
||||
* @param string $commitish Commitish reference; branch, tag, SHA1, etc.
|
||||
* @param string $path Path whose existence we want to verify.
|
||||
*
|
||||
* GRIPE Arguably belongs in Gitter, as it's generally useful functionality.
|
||||
* Also, this really may not be the best way to do this.
|
||||
*/
|
||||
public function pathExists($commitish, $path)
|
||||
{
|
||||
$output = $this->getClient()->run($this, "ls-tree $commitish '$path'");
|
||||
|
||||
if (strlen($output) > 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ use Silex\ServiceProviderInterface;
|
||||
|
||||
class GitServiceProvider implements ServiceProviderInterface
|
||||
{
|
||||
|
||||
/**
|
||||
* Register the Git\Client on the Application ServiceProvider
|
||||
*
|
||||
@@ -19,6 +20,8 @@ class GitServiceProvider implements ServiceProviderInterface
|
||||
$app['git'] = function () use ($app) {
|
||||
$options['path'] = $app['git.client'];
|
||||
$options['hidden'] = $app['git.hidden'];
|
||||
$options['ini.file'] = $app['ini.file'];
|
||||
$options['default_branch'] = $app['git.default_branch'];
|
||||
|
||||
return new Client($options);
|
||||
};
|
||||
|
||||
@@ -52,6 +52,7 @@ class Repository
|
||||
'r' => 'r',
|
||||
'sh' => 'shell',
|
||||
'ss' => 'scheme',
|
||||
'scala' => 'text/x-scala',
|
||||
'scm' => 'scheme',
|
||||
'sls' => 'scheme',
|
||||
'sps' => 'scheme',
|
||||
@@ -160,10 +161,12 @@ class Repository
|
||||
return false;
|
||||
}
|
||||
|
||||
public function getReadme($repo, $branch = 'master')
|
||||
public function getReadme($repository, $branch = null)
|
||||
{
|
||||
$repository = $this->app['git']->getRepository($this->app['git.repos'] . $repo);
|
||||
$files = $repository->getTree($branch)->output();
|
||||
if ($branch === null) {
|
||||
$branch = $repository->getHead();
|
||||
}
|
||||
|
||||
foreach ($files as $file) {
|
||||
if (preg_match('/^readme*/i', $file['name'])) {
|
||||
@@ -185,7 +188,7 @@ class Repository
|
||||
* @param string $tree
|
||||
* @return array
|
||||
*/
|
||||
public function extractRef($repository, $branch='', $tree='')
|
||||
public function extractRef($repository, $branch = '', $tree = '')
|
||||
{
|
||||
$branch = trim($branch, '/');
|
||||
$tree = trim($tree, '/');
|
||||
@@ -196,26 +199,24 @@ class Repository
|
||||
$branch = $matches[1];
|
||||
} else {
|
||||
// Otherwise, attempt to detect the ref using a list of the project's branches and tags
|
||||
$valid_refs = array_merge((array) $repository->getBranches(), (array) $repository->getTags());
|
||||
foreach ($valid_refs as $k => $v) {
|
||||
if (!preg_match("#{$v}/#", $input)) {
|
||||
unset($valid_refs[$k]);
|
||||
$validRefs = array_merge((array) $repository->getBranches(), (array) $repository->getTags());
|
||||
foreach ($validRefs as $key => $ref) {
|
||||
if (!preg_match(sprintf("#^%s/#", preg_quote($ref, '#')), $input)) {
|
||||
unset($validRefs[$key]);
|
||||
}
|
||||
}
|
||||
|
||||
// No exact ref match, so just try our best
|
||||
if (count($valid_refs) > 1) {
|
||||
if (count($validRefs) > 1) {
|
||||
preg_match('/([^\/]+)(.*)/', $input, $matches);
|
||||
$branch = preg_replace('/^\/|\/$/', '', $matches[0]);
|
||||
$branch = preg_replace('/^\/|\/$/', '', $matches[1]);
|
||||
} else {
|
||||
// Extract branch name
|
||||
$branch = array_shift($valid_refs);
|
||||
$branch = array_shift($validRefs);
|
||||
}
|
||||
}
|
||||
|
||||
$tree = trim(str_replace($branch, "", $input), "/");
|
||||
|
||||
return array($branch, $tree);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
<?php
|
||||
|
||||
namespace Gitlist\Util;
|
||||
namespace GitList\Util;
|
||||
|
||||
use Silex\Application;
|
||||
use GitList\Exception\EmptyRepositoryException;
|
||||
|
||||
class Routing
|
||||
{
|
||||
@@ -13,36 +14,145 @@ class Routing
|
||||
$this->app = $app;
|
||||
}
|
||||
|
||||
/* @brief Return $commitish, $path parsed from $commitishPath, based on
|
||||
* what's in $repo. Raise a 404 if $branchpath does not represent a
|
||||
* valid branch and path.
|
||||
*
|
||||
* A helper for parsing routes that use commit-ish names and paths
|
||||
* separated by /, since route regexes are not enough to get that right.
|
||||
*/
|
||||
public function parseCommitishPathParam($commitishPath, $repo)
|
||||
{
|
||||
$app = $this->app;
|
||||
$repository = $app['git']->getRepositoryFromName($app['git.repos'], $repo);
|
||||
|
||||
$commitish = null;
|
||||
$path = null;
|
||||
|
||||
$slashPosition = strpos($commitishPath, '/');
|
||||
if (strlen($commitishPath) >= 40 &&
|
||||
($slashPosition === false ||
|
||||
$slashPosition === 40)) {
|
||||
// We may have a commit hash as our commitish.
|
||||
$hash = substr($commitishPath, 0, 40);
|
||||
if ($repository->hasCommit($hash)) {
|
||||
$commitish = $hash;
|
||||
}
|
||||
}
|
||||
|
||||
if ($commitish === null) {
|
||||
$branches = $repository->getBranches();
|
||||
|
||||
$tags = $repository->getTags();
|
||||
if ($tags !== null && count($tags) > 0) {
|
||||
$branches = array_merge($branches, $tags);
|
||||
}
|
||||
|
||||
$matchedBranch = null;
|
||||
$matchedBranchLength = 0;
|
||||
foreach ($branches as $branch) {
|
||||
if (strpos($commitishPath, $branch) === 0 &&
|
||||
strlen($branch) > $matchedBranchLength) {
|
||||
$matchedBranch = $branch;
|
||||
$matchedBranchLength = strlen($matchedBranch);
|
||||
}
|
||||
}
|
||||
|
||||
if ($matchedBranch === null) {
|
||||
throw new EmptyRepositoryException('This repository is currently empty. There are no commits.');
|
||||
}
|
||||
|
||||
$commitish = $matchedBranch;
|
||||
}
|
||||
|
||||
$commitishLength = strlen($commitish);
|
||||
$path = substr($commitishPath, $commitishLength);
|
||||
if (strpos($path, '/') === 0) {
|
||||
$path = substr($path, 1);
|
||||
}
|
||||
|
||||
return array($commitish, $path);
|
||||
}
|
||||
|
||||
public function getBranchRegex()
|
||||
{
|
||||
static $branchRegex = null;
|
||||
|
||||
if ($branchRegex === null) {
|
||||
$branchRegex = '(?!/|.*([/.]\.|//|@\{|\\\\))[^\040\177 ~^:?*\[]+(?<!\.lock|[/.])';
|
||||
}
|
||||
|
||||
return $branchRegex;
|
||||
}
|
||||
|
||||
public function getCommitishPathRegex()
|
||||
{
|
||||
static $commitishPathRegex = null;
|
||||
|
||||
if ($commitishPathRegex === null) {
|
||||
$commitishPathRegex = '.+';
|
||||
}
|
||||
|
||||
return $commitishPathRegex;
|
||||
}
|
||||
|
||||
public function getRepositoryRegex()
|
||||
{
|
||||
static $regex = null;
|
||||
|
||||
if ($regex === null) {
|
||||
$app = $this->app;
|
||||
$self = $this;
|
||||
$quotedPaths = array_map(
|
||||
function ($repo) use ($app) {
|
||||
return preg_quote($app['util.routing']->getRelativePath($repo['path']), '#');
|
||||
function ($repo) use ($app, $self) {
|
||||
$repoName = $repo['name'];
|
||||
//Windows
|
||||
if ($self->isWindows()){
|
||||
$repoName = str_replace('\\', '\\\\',$repoName);
|
||||
}
|
||||
return $repoName;
|
||||
},
|
||||
$this->app['git']->getRepositories($this->app['git.repos'])
|
||||
);
|
||||
usort($quotedPaths, function ($a, $b) { return strlen($b) - strlen($a); });
|
||||
|
||||
usort(
|
||||
$quotedPaths,
|
||||
function ($a, $b) {
|
||||
return strlen($b) - strlen($a);
|
||||
}
|
||||
);
|
||||
|
||||
$regex = implode('|', $quotedPaths);
|
||||
}
|
||||
|
||||
return $regex;
|
||||
}
|
||||
|
||||
|
||||
public function isWindows()
|
||||
{
|
||||
switch (PHP_OS) {
|
||||
case 'WIN32':
|
||||
case 'WINNT':
|
||||
case 'Windows':
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Strips the base path from a full repository path
|
||||
*
|
||||
* @param string $repoPath Full path to the repository
|
||||
* @param string $repoPath Full path to the repository
|
||||
* @return string Relative path to the repository from git.repositories
|
||||
*/
|
||||
public function getRelativePath($repoPath)
|
||||
{
|
||||
if (strpos($repoPath, $this->app['git.repos']) === 0) {
|
||||
$relativePath = substr($repoPath, strlen($this->app['git.repos']));
|
||||
return ltrim($relativePath, '/');
|
||||
|
||||
return ltrim(strtr($relativePath, '\\', '/'), '/');
|
||||
} else {
|
||||
throw new \InvalidArgumentException(
|
||||
sprintf("Path '%s' does not match configured repository directory", $repoPath)
|
||||
@@ -50,3 +160,4 @@ class Routing
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
use Silex\WebTestCase;
|
||||
use Symfony\Component\Filesystem\Filesystem;
|
||||
use Gitter\Client;
|
||||
use GitList\Git\Client;
|
||||
|
||||
class InterfaceTest extends WebTestCase
|
||||
{
|
||||
@@ -16,10 +16,10 @@ class InterfaceTest extends WebTestCase
|
||||
} elseif (getenv('TMPDIR')) {
|
||||
self::$tmpdir = getenv('TMPDIR');
|
||||
} else {
|
||||
self::$tmpdir = '/tmp';
|
||||
self::$tmpdir = DIRECTORY_SEPARATOR . 'tmp';
|
||||
}
|
||||
|
||||
self::$tmpdir .= '/gitlist_' . md5(time() . mt_rand()) . '/';
|
||||
self::$tmpdir .= DIRECTORY_SEPARATOR . 'gitlist_' . md5(time() . mt_rand()) . DIRECTORY_SEPARATOR;
|
||||
|
||||
$fs = new Filesystem();
|
||||
$fs->mkdir(self::$tmpdir);
|
||||
@@ -30,6 +30,12 @@ class InterfaceTest extends WebTestCase
|
||||
|
||||
$options['path'] = getenv('GIT_CLIENT') ?: '/usr/bin/git';
|
||||
$options['hidden'] = array(self::$tmpdir . '/hiddenrepo');
|
||||
$options['default_branch'] = 'master';
|
||||
$options['ini.file'] = "config.ini";
|
||||
|
||||
$cacheDir = self::$tmpdir . DIRECTORY_SEPARATOR . 'cache';
|
||||
$fs->mkdir($cacheDir);
|
||||
|
||||
$git = new Client($options);
|
||||
|
||||
self::$gitPath = $options['path'];
|
||||
@@ -49,13 +55,16 @@ class InterfaceTest extends WebTestCase
|
||||
|
||||
// foobar repository fixture
|
||||
$git->createRepository(self::$tmpdir . 'foobar');
|
||||
$repository = $git->getRepository(self::$tmpdir . '/foobar');
|
||||
$repository = $git->getRepository(self::$tmpdir . 'foobar');
|
||||
|
||||
file_put_contents(self::$tmpdir . 'foobar/bar.json', "{\n\"name\": \"foobar\"\n}");
|
||||
file_put_contents(self::$tmpdir . 'foobar/.git/description', 'This is a test repo!');
|
||||
$fs->mkdir(self::$tmpdir . 'foobar/myfolder');
|
||||
$fs->mkdir(self::$tmpdir . 'foobar/testfolder');
|
||||
file_put_contents(self::$tmpdir . 'foobar/myfolder/mytest.php', "<?php\necho 'Hello World'; // This is my test");
|
||||
file_put_contents(self::$tmpdir . 'foobar/testfolder/test.php', "<?php\necho 'Hello World'; // This is a test");
|
||||
file_put_contents(self::$tmpdir . 'foobar/myfolder/mytest.php',
|
||||
"<?php\necho 'Hello World'; // This is my test");
|
||||
file_put_contents(self::$tmpdir . 'foobar/testfolder/test.php',
|
||||
"<?php\necho 'Hello World'; // This is a test");
|
||||
$repository->setConfig('user.name', 'Luke Skywalker');
|
||||
$repository->setConfig('user.email', 'luke@rebel.org');
|
||||
$repository->addAll();
|
||||
@@ -65,7 +74,7 @@ class InterfaceTest extends WebTestCase
|
||||
$nested_dir = self::$tmpdir . 'nested/';
|
||||
$fs->mkdir($nested_dir);
|
||||
$git->createRepository($nested_dir . 'NestedRepo');
|
||||
$repository = $git->getRepository($nested_dir . '/NestedRepo');
|
||||
$repository = $git->getRepository($nested_dir . 'NestedRepo');
|
||||
file_put_contents($nested_dir . 'NestedRepo/.git/description', 'This is a NESTED test repo!');
|
||||
file_put_contents($nested_dir . 'NestedRepo/README.txt', 'NESTED TEST REPO README');
|
||||
$repository->setConfig('user.name', 'Luke Skywalker');
|
||||
@@ -79,19 +88,43 @@ class InterfaceTest extends WebTestCase
|
||||
$repository->commit("Changing branch");
|
||||
$repository->checkout("master");
|
||||
|
||||
// master-less repository fixture
|
||||
$git->createRepository(self::$tmpdir . 'develop');
|
||||
$repository = $git->getRepository(self::$tmpdir . 'develop');
|
||||
$repository->setConfig('user.name', 'Luke Skywalker');
|
||||
$repository->setConfig('user.email', 'luke@rebel.org');
|
||||
file_put_contents(self::$tmpdir . 'develop/README.md', "## develop\ndevelop is a *test* repository!");
|
||||
$repository->addAll();
|
||||
$repository->commit("First commit");
|
||||
$repository->createBranch("develop");
|
||||
$repository = $repository->checkout('develop');
|
||||
|
||||
file_put_contents(self::$tmpdir . 'develop/test.php', "<?php\necho 'Hello World'; // This is a test");
|
||||
$repository->setConfig('user.name', 'Luke Skywalker');
|
||||
$repository->setConfig('user.email', 'luke@rebel.org');
|
||||
$repository->addAll();
|
||||
$repository->commit("Initial commit");
|
||||
|
||||
// Detached HEAD repository fixture
|
||||
$git->createRepository(self::$tmpdir . 'detached-head');
|
||||
$repository = $git->getRepository(self::$tmpdir . 'detached-head');
|
||||
$repository->setConfig('user.name', 'Luke Skywalker');
|
||||
$repository->setConfig('user.email', 'luke@rebel.org');
|
||||
file_put_contents(self::$tmpdir . 'detached-head/README.md', "## detached head\ndetached-head is a *test* repository!");
|
||||
$repository->addAll();
|
||||
$repository->commit("First commit");
|
||||
$repository->checkout('HEAD');
|
||||
}
|
||||
|
||||
public function createApplication()
|
||||
{
|
||||
$config = new \GitList\Config(array(
|
||||
'git' => array(
|
||||
'client' => self::$gitPath,
|
||||
'repositories' => self::$tmpdir,
|
||||
),
|
||||
'app' => array(
|
||||
'debug' => true,
|
||||
),
|
||||
));
|
||||
$config = new GitList\Config;
|
||||
$config->set('app', 'debug', true);
|
||||
$config->set('app', 'debug', false);
|
||||
$config->set('git', 'client', self::$gitPath);
|
||||
$config->set('git', 'default_branch', 'master');
|
||||
$config->set('git', 'repositories', array(self::$tmpdir));
|
||||
|
||||
$app = require 'boot.php';
|
||||
return $app;
|
||||
}
|
||||
@@ -103,15 +136,28 @@ class InterfaceTest extends WebTestCase
|
||||
|
||||
$this->assertTrue($client->getResponse()->isOk());
|
||||
$this->assertCount(1, $crawler->filter('title:contains("GitList")'));
|
||||
$this->assertCount(1, $crawler->filter('div.repository-header:contains("GitTest")'));
|
||||
|
||||
$this->assertCount(1, $crawler->filter('div.repository-header a:contains("GitTest")'));
|
||||
$this->assertEquals('/GitTest/', $crawler->filter('.repository-header a')->eq(0)->attr('href'));
|
||||
$this->assertEquals('/GitTest/master/rss/', $crawler->filter('.repository-header a')->eq(1)->attr('href'));
|
||||
$this->assertEquals('/nested/NestedRepo/', $crawler->filter('.repository-header a')->eq(2)->attr('href'));
|
||||
$this->assertEquals('/nested/NestedRepo/master/rss/', $crawler->filter('.repository-header a')->eq(3)->attr('href'));
|
||||
|
||||
$this->assertCount(1, $crawler->filter('div.repository-header a:contains("detached-head")'));
|
||||
$this->assertEquals('/detached-head/', $crawler->filter('.repository-header a')->eq(2)->attr('href'));
|
||||
$this->assertEquals('/detached-head/master/rss/', $crawler->filter('.repository-header a')->eq(3)->attr('href'));
|
||||
|
||||
$this->assertCount(1, $crawler->filter('div.repository-header a:contains("develop")'));
|
||||
$this->assertEquals('/develop/', $crawler->filter('.repository-header a')->eq(4)->attr('href'));
|
||||
$this->assertEquals('/develop/master/rss/', $crawler->filter('.repository-header a')->eq(5)->attr('href'));
|
||||
|
||||
$this->assertCount(1, $crawler->filter('div.repository-header:contains("foobar")'));
|
||||
$this->assertCount(1, $crawler->filter('div.repository-body:contains("This is a test repo!")'));
|
||||
$this->assertEquals('/foobar/', $crawler->filter('.repository-header a')->eq(4)->attr('href'));
|
||||
$this->assertEquals('/foobar/master/rss/', $crawler->filter('.repository-header a')->eq(5)->attr('href'));
|
||||
$this->assertEquals('/foobar/', $crawler->filter('.repository-header a')->eq(6)->attr('href'));
|
||||
$this->assertEquals('/foobar/master/rss/', $crawler->filter('.repository-header a')->eq(7)->attr('href'));
|
||||
|
||||
$this->assertCount(1, $crawler->filter('div.repository-header a:contains("nested/NestedRepo")'));
|
||||
$this->assertEquals('/nested/NestedRepo/', $crawler->filter('.repository-header a')->eq(8)->attr('href'));
|
||||
$this->assertEquals('/nested/NestedRepo/master/rss/', $crawler->filter('.repository-header a')->eq(9)->attr('href'));
|
||||
$this->assertCount(1, $crawler->filter('div.repository-body:contains("This is a NESTED test repo!")'));
|
||||
}
|
||||
|
||||
public function testRepositoryPage()
|
||||
@@ -122,8 +168,8 @@ class InterfaceTest extends WebTestCase
|
||||
$this->assertTrue($client->getResponse()->isOk());
|
||||
$this->assertCount(1, $crawler->filter('.tree tr:contains("README.md")'));
|
||||
$this->assertCount(1, $crawler->filter('.tree tr:contains("test.php")'));
|
||||
$this->assertCount(1, $crawler->filter('.readme-header:contains("README.md")'));
|
||||
$this->assertEquals("## GitTest\nGitTest is a *test* repository!", $crawler->filter('#readme-content')->eq(0)->text());
|
||||
$this->assertCount(1, $crawler->filter('.md-header:contains("README.md")'));
|
||||
$this->assertEquals("## GitTest\nGitTest is a *test* repository!", $crawler->filter('#md-content')->eq(0)->text());
|
||||
$this->assertEquals('/GitTest/blob/master/README.md', $crawler->filter('.tree tr td')->eq(0)->filter('a')->eq(0)->attr('href'));
|
||||
$this->assertEquals('/GitTest/blob/master/test.php', $crawler->filter('.tree tr td')->eq(3)->filter('a')->eq(0)->attr('href'));
|
||||
|
||||
@@ -140,7 +186,7 @@ class InterfaceTest extends WebTestCase
|
||||
$this->assertEquals('/foobar/tree/master/myfolder/', $crawler->filter('.tree tr td')->eq(0)->filter('a')->eq(0)->attr('href'));
|
||||
$this->assertEquals('/foobar/tree/master/testfolder/', $crawler->filter('.tree tr td')->eq(3)->filter('a')->eq(0)->attr('href'));
|
||||
$this->assertEquals('/foobar/blob/master/bar.json', $crawler->filter('.tree tr td')->eq(6)->filter('a')->eq(0)->attr('href'));
|
||||
$this->assertCount(0, $crawler->filter('.readme-header'));
|
||||
$this->assertCount(0, $crawler->filter('.md-header'));
|
||||
$this->assertEquals('master', $crawler->filter('.dropdown-menu li')->eq(1)->text());
|
||||
}
|
||||
|
||||
@@ -149,11 +195,15 @@ class InterfaceTest extends WebTestCase
|
||||
$client = $this->createClient();
|
||||
|
||||
$crawler = $client->request('GET', '/GitTest/blob/master/test.php');
|
||||
|
||||
$this->assertTrue($client->getResponse()->isOk());
|
||||
$this->assertCount(1, $crawler->filter('.breadcrumb .active:contains("test.php")'));
|
||||
$this->assertEquals('/GitTest/raw/master/test.php', $crawler->filter('.source-header .btn-group a')->eq(0)->attr('href'));
|
||||
$this->assertEquals('/GitTest/blame/master/test.php', $crawler->filter('.source-header .btn-group a')->eq(1)->attr('href'));
|
||||
$this->assertEquals('/GitTest/commits/master/test.php', $crawler->filter('.source-header .btn-group a')->eq(2)->attr('href'));
|
||||
$this->assertEquals('/GitTest/raw/master/test.php',
|
||||
$crawler->filter('.source-header .btn-group a')->eq(0)->attr('href'));
|
||||
$this->assertEquals('/GitTest/blame/master/test.php',
|
||||
$crawler->filter('.source-header .btn-group a')->eq(1)->attr('href'));
|
||||
$this->assertEquals('/GitTest/commits/master/test.php',
|
||||
$crawler->filter('.source-header .btn-group a')->eq(2)->attr('href'));
|
||||
}
|
||||
|
||||
public function testRawPage()
|
||||
@@ -172,12 +222,14 @@ class InterfaceTest extends WebTestCase
|
||||
$crawler = $client->request('GET', '/GitTest/blame/master/test.php');
|
||||
$this->assertTrue($client->getResponse()->isOk());
|
||||
$this->assertCount(1, $crawler->filter('.source-header .meta:contains("test.php")'));
|
||||
$this->assertRegexp('/\/GitTest\/commit\/[a-zA-Z0-9%]+\//', $crawler->filter('.blame-view .commit')->eq(0)->filter('a')->attr('href'));
|
||||
$this->assertRegexp('/\/GitTest\/commit\/[a-zA-Z0-9%]+/',
|
||||
$crawler->filter('.blame-view .commit')->eq(0)->filter('a')->attr('href'));
|
||||
|
||||
$crawler = $client->request('GET', '/foobar/blame/master/bar.json');
|
||||
$this->assertTrue($client->getResponse()->isOk());
|
||||
$this->assertCount(1, $crawler->filter('.source-header .meta:contains("bar.json")'));
|
||||
$this->assertRegexp('/\/foobar\/commit\/[a-zA-Z0-9%]+\//', $crawler->filter('.blame-view .commit')->eq(0)->filter('a')->attr('href'));
|
||||
$this->assertRegexp('/\/foobar\/commit\/[a-zA-Z0-9%]+/',
|
||||
$crawler->filter('.blame-view .commit')->eq(0)->filter('a')->attr('href'));
|
||||
}
|
||||
|
||||
public function testHistoryPage()
|
||||
@@ -226,8 +278,10 @@ class InterfaceTest extends WebTestCase
|
||||
{
|
||||
$client = $this->createClient();
|
||||
|
||||
$crawler = $client->request('GET', '/GitTest/master/rss/');
|
||||
$this->assertTrue($client->getResponse()->isOk());
|
||||
$client->request('GET', '/GitTest/master/rss/');
|
||||
$response = $client->getResponse();
|
||||
|
||||
$this->assertTrue($response->isOk());
|
||||
$this->assertRegexp('/Latest commits in GitTest:master/', $client->getResponse()->getContent());
|
||||
$this->assertRegexp('/Initial commit/', $client->getResponse()->getContent());
|
||||
}
|
||||
@@ -236,11 +290,21 @@ class InterfaceTest extends WebTestCase
|
||||
{
|
||||
$client = $this->createClient();
|
||||
|
||||
$crawler = $client->request('GET', '/nested/NestedRepo/');
|
||||
$this->assertTrue($client->getResponse()->isOk());
|
||||
$client->request('GET', '/nested/NestedRepo/');
|
||||
$response = $client->getResponse();
|
||||
|
||||
$this->assertTrue($response->isOk());
|
||||
$this->assertRegexp('/NESTED TEST REPO README/', $client->getResponse()->getContent());
|
||||
}
|
||||
|
||||
public function testDevelopRepo()
|
||||
{
|
||||
$client = $this->createClient();
|
||||
|
||||
$crawler = $client->request('GET', '/develop/');
|
||||
$this->assertTrue($client->getResponse()->isOk());
|
||||
}
|
||||
|
||||
public function testNestedRepoBranch()
|
||||
{
|
||||
$client = $this->createClient();
|
||||
@@ -256,3 +320,4 @@ class InterfaceTest extends WebTestCase
|
||||
$fs->remove(self::$tmpdir);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
<table class="blame-view">
|
||||
{% for blame in blames %}
|
||||
<tr>
|
||||
<td class="commit"><a href="{{ path('commit', {repo: repo, commit: blame.commit}) }}">{{ blame.commit }}</a></td>
|
||||
<td class="commit"><a href="{{ path('commit', {repo: repo, commit: blame.commit}) }}">{{ blame.commitShort }}</a></td>
|
||||
<td><pre>{{ blame.line }}</pre></td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
<ul class="breadcrumb">
|
||||
<li><a href="{{ path('repository', {repo: repo}) }}">{{ repo }}</a></li>
|
||||
<li><a href="{{ path('tree', {repo: repo, commitishPath: branch}) }}">{{ repo }}</a></li>
|
||||
{% for breadcrumb in breadcrumbs %}
|
||||
<span class="divider">/</span>
|
||||
<li{% if loop.last %} class="active"{% endif %}>{% if not loop.last %}<a href="{{ path('tree', {repo: repo, branch: branch, tree: breadcrumb.path}) }}">{{ breadcrumb.dir }}</a>{% endif %}{% if loop.last %}{{ breadcrumb.dir }}{% endif %}</li>
|
||||
<li{% if loop.last %} class="active"{% endif %}>{% if not loop.last %}<a href="{{ path('tree', {repo: repo, commitishPath: branch ~ '/' ~ breadcrumb.path}) }}">{{ breadcrumb.dir }}</a>{% endif %}{% if loop.last %}{{ breadcrumb.dir }}{% endif %}</li>
|
||||
{% endfor %}
|
||||
|
||||
{% block extra %}{% endblock %}
|
||||
|
||||
@@ -13,8 +13,11 @@
|
||||
<h4>{{ commit.message }}</h4>
|
||||
</div>
|
||||
<div class="commit-body">
|
||||
<img src="https://gravatar.com/avatar/{{ commit.author.email | md5 }}?s=32" class="pull-left space-right" />
|
||||
<span><a href="mailto:{{ commit.author.email }}">{{ commit.author.name }}</a> authored in {{ commit.date | date('d/m/Y \\a\\t H:i:s') }}<br />Showing {{ commit.changedFiles }} changed files</span>
|
||||
{% if commit.body is not empty %}
|
||||
<p>{{ commit.body | nl2br }}</p>
|
||||
{% endif %}
|
||||
<img src="https://gravatar.com/avatar/{{ commit.author.email | lower | md5 }}?s=32" class="pull-left space-right" />
|
||||
<span><a href="mailto:{{ commit.author.email }}">{{ commit.author.name }}</a> authored on {{ commit.date | date('d/m/Y \\a\\t H:i:s') }}<br />Showing {{ commit.changedFiles }} changed files</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -30,8 +33,8 @@
|
||||
<div class="meta"><a name="{{ loop.index }}">{{ diff.file }}</div>
|
||||
|
||||
<div class="btn-group pull-right">
|
||||
<a href="{{ path('commits', {repo: repo, branch: commit.hash, file: diff.file}) }}" class="btn btn-small"><i class="icon-list-alt"></i> History</a>
|
||||
<a href="{{ path('blob', {repo: repo, branch: commit.hash, file: diff.file}) }}" class="btn btn-small"><i class="icon-file"></i> View file @ {{ commit.shortHash }}</a>
|
||||
<a href="{{ path('commits', {repo: repo, commitishPath: commit.hash ~ '/' ~ diff.file}) }}" class="btn btn-small"><i class="icon-list-alt"></i> History</a>
|
||||
<a href="{{ path('blob', {repo: repo, commitishPath: commit.hash ~'/' ~ diff.file}) }}" class="btn btn-small"><i class="icon-file"></i> View file @ {{ commit.shortHash }}</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
{% if commits %}
|
||||
{% for date, commit in commits %}
|
||||
<table class="table table-striped table-bordered">
|
||||
<thead>
|
||||
@@ -8,17 +9,20 @@
|
||||
<tbody>
|
||||
{% for item in commit %}
|
||||
<tr>
|
||||
<td width="5%"><img src="http://gravatar.com/avatar/{{ item.author.email | md5 }}?s=40" /></td>
|
||||
<td width="5%"><img src="https://gravatar.com/avatar/{{ item.author.email | lower | md5 }}?s=40" /></td>
|
||||
<td width="95%">
|
||||
<span class="pull-right"><a class="btn btn-small" href="{{ path('commit', {repo: repo, commit: item.shortHash}) }}"><i class="icon-list-alt"></i> View {{ item.shortHash }}</a></span>
|
||||
<span class="pull-right"><a class="btn btn-small" href="{{ path('commit', {repo: repo, commit: item.hash}) }}"><i class="icon-list-alt"></i> View {{ item.shortHash }}</a></span>
|
||||
<h4>{{ item.message }}</h4>
|
||||
<span><a href="mailto:{{ item.author.email }}">{{ item.author.name }}</a> authored in {{ item.date | date('d/m/Y \\a\\t H:i:s') }}</span>
|
||||
<span><a href="mailto:{{ item.author.email }}">{{ item.author.name }}</a> authored on {{ item.date | date('d/m/Y \\a\\t H:i:s') }}</span>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
<p>No results found.</p>
|
||||
{% endif %}
|
||||
|
||||
{% if page != 'searchcommits' %}
|
||||
<ul class="pager">
|
||||
@@ -33,4 +37,4 @@
|
||||
</li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
@@ -12,19 +12,19 @@
|
||||
<div class="meta"></div>
|
||||
|
||||
<div class="btn-group pull-right">
|
||||
<a href="{{ path('blob_raw', {repo: repo, branch: branch, file: file}) }}" class="btn btn-small"><i class="icon-file"></i> Raw</a>
|
||||
<a href="{{ path('blame', {repo: repo, branch: branch, file: file}) }}" class="btn btn-small"><i class="icon-bullhorn"></i> Blame</a>
|
||||
<a href="{{ path('commits', {repo: repo, branch: branch, file: file}) }}" class="btn btn-small"><i class="icon-list-alt"></i> History</a>
|
||||
<a href="{{ path('blob_raw', {repo: repo, commitishPath: branch ~ '/' ~ file}) }}" class="btn btn-small"><i class="icon-file"></i> Raw</a>
|
||||
<a href="{{ path('blame', {repo: repo, commitishPath: branch ~ '/' ~ file}) }}" class="btn btn-small"><i class="icon-bullhorn"></i> Blame</a>
|
||||
<a href="{{ path('commits', {repo: repo, commitishPath: branch ~ '/' ~ file}) }}" class="btn btn-small"><i class="icon-list-alt"></i> History</a>
|
||||
</div>
|
||||
</div>
|
||||
{% if fileType == 'image' %}
|
||||
<center><img src="{{ path('blob_raw', {repo: repo, branch: branch, file: file}) }}" alt="{{ file }}" class="image-blob" /></center>
|
||||
<center><img src="{{ path('blob_raw', {repo: repo, commitishPath: branch ~ '/' ~ file}) }}" alt="{{ file }}" class="image-blob" /></center>
|
||||
|
||||
{% elseif fileType == 'markdown' %}
|
||||
<div class="readme-view"><div id="readme-content">{{ blob }}</div></div>
|
||||
<div class="md-view"><div id="md-content">{{ blob }}</div></div>
|
||||
|
||||
{% else %}
|
||||
<pre id="sourcecode" language="{{ fileType }}">{{ blob }}</pre>
|
||||
<pre id="sourcecode" language="{{ fileType }}">{{ blob|htmlentities|raw }}</pre>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
|
||||
@@ -9,11 +9,15 @@
|
||||
{% for repository in repositories %}
|
||||
<div class="repository">
|
||||
<div class="repository-header">
|
||||
<i class="icon-folder-open icon-spaced"></i> <a href="{{ path('repository', {repo: repository.relativePath}) }}">{{ repository.name }}</a>
|
||||
<a href="{{ path('rss', {repo: repository.relativePath, branch: 'master'}) }}"><i class="rss pull-right"></i></a>
|
||||
<i class="icon-folder-open icon-spaced"></i> <a href="{{ path('repository', {repo: repository.name}) }}">{{ repository.name }}</a>
|
||||
<a href="{{ path('rss', {repo: repository.name, branch: 'master'}) }}"><i class="rss pull-right"></i></a>
|
||||
</div>
|
||||
<div class="repository-body">
|
||||
{% if repository.description %}
|
||||
<p>{{ repository.description }}</p>
|
||||
{% else %}
|
||||
<p>There is no repository description file. Please, create one to remove this message.</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
<meta charset="UTF-8" />
|
||||
<title>{% block title %}Welcome!{% endblock %}</title>
|
||||
<link rel="stylesheet" type="text/css" href="{{ app.request.basepath }}/web/css/style.css">
|
||||
<link rel="shortcut icon" type="image/png" href="{{ app.request.basepath }}/web/img/favicon.png" />
|
||||
<!--[if lt IE 9]>
|
||||
<script src="{{ app.request.basepath }}/web/js/html5.js"></script>
|
||||
<![endif]-->
|
||||
@@ -12,9 +13,11 @@
|
||||
<body>
|
||||
{% block body %}{% endblock %}
|
||||
<script src="{{ app.request.basepath }}/web/js/jquery.js"></script>
|
||||
<script src="{{ app.request.basepath }}/web/js/raphael.js"></script>
|
||||
<script src="{{ app.request.basepath }}/web/js/bootstrap.js"></script>
|
||||
<script src="{{ app.request.basepath }}/web/js/codemirror.js"></script>
|
||||
<script src="{{ app.request.basepath }}/web/js/showdown.js"></script>
|
||||
<script src="{{ app.request.basepath }}/web/js/main.js"></script>
|
||||
<script src="{{ app.request.basepath }}/web/js/networkGraph.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -6,8 +6,8 @@
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="span12">
|
||||
{% if page == 'commits' %}
|
||||
<form class="form-search pull-right" action="{{ app.request.basepath }}/{{repo}}/commits/search" method="POST">
|
||||
{% if page in ['commits', 'searchcommits'] %}
|
||||
<form class="form-search pull-right" action="{{ app.request.basepath }}/{{repo}}/commits/{{branch}}/search" method="POST">
|
||||
<input type="text" name="query" class="input-medium search-query" placeholder="Search commits...">
|
||||
</form>
|
||||
{% else %}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<ul class="nav nav-tabs">
|
||||
<li{% if page == 'files' %} class="active"{% endif %}><a href="{{ path('branch', {repo: repo, branch: branch}) }}">Files</a></li>
|
||||
<li{% if page == 'commits' %} class="active"{% endif %}><a href="{{ path('commits', {repo: repo, branch: branch}) }}">Commits</a></li>
|
||||
<li{% if page in ['commits', 'searchcommits'] %} class="active"{% endif %}><a href="{{ path('commits', {repo: repo, commitishPath: branch}) }}">Commits</a></li>
|
||||
<li{% if page == 'stats' %} class="active"{% endif %}><a href="{{ path('stats', {repo: repo, branch: branch}) }}">Stats</a></li>
|
||||
<li{% if page == 'network' %} class="active"{% endif %}><a href="{{ path('network', {repo: repo, branch: branch}) }}">Network</a></li>
|
||||
</ul>
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
<div class="nav-collapse">
|
||||
<ul class="nav pull-right">
|
||||
<li><a href="http://gitlist.org/">About</a></li>
|
||||
<li><a href="{{ path('homepage') }}refresh">Refresh</a></li>
|
||||
<li><a href="https://github.com/klaussilveira/gitlist/issues/new">Report bug</a></li>
|
||||
<li><a href="https://github.com/klaussilveira/gitlist/wiki">Help</a></li>
|
||||
</ul>
|
||||
|
||||
23
views/network.twig
Normal file
23
views/network.twig
Normal file
@@ -0,0 +1,23 @@
|
||||
{% extends 'layout_page.twig' %}
|
||||
|
||||
{% set page = 'network' %}
|
||||
|
||||
{% block title %}GitList{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
{% include 'breadcrumb.twig' with {breadcrumbs: [{dir: 'Network', path:''}]} %}
|
||||
<div class="network-view">
|
||||
<div class="network-header">
|
||||
<div class="meta">Network Graph of {{ repo }} / {{ commitishPath }}</div>
|
||||
</div>
|
||||
|
||||
<div class="network-graph" data-source="{{ path('networkData', {repo: repo, commitishPath: commitishPath}) }}">
|
||||
{#<div class="network-graph" data-source="/dummynetwork.json">#}
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<hr />
|
||||
{% endblock %}
|
||||
@@ -3,13 +3,13 @@
|
||||
<channel>
|
||||
<title>Latest commits in {{ repo }}:{{ branch }}</title>
|
||||
<description>RSS provided by GitList</description>
|
||||
<link>{{ path('homepage') }}</link>
|
||||
<link>{{ url('homepage') }}</link>
|
||||
|
||||
{% for commit in commits %}
|
||||
<item>
|
||||
<title>{{ commit.message }}</title>
|
||||
<description>{{ commit.author.name }} authored {{ commit.shortHash }} in {{ commit.date | date('d/m/Y \\a\\t H:i:s') }}</description>
|
||||
<link>{{ path('commit', {repo: repo, commit: commit.shortHash}) }}</link>
|
||||
<link>{{ url('commit', {repo: repo, commit: commit.hash}) }}</link>
|
||||
<pubDate>{{ commit.date | date('r') }}</pubDate>
|
||||
</item>
|
||||
{% endfor %}
|
||||
|
||||
@@ -28,7 +28,7 @@
|
||||
<tbody>
|
||||
{% for result in results %}
|
||||
<tr>
|
||||
<td><i class="icon-file icon-spaced"></i> <a href="{{ path('blob', {repo: repo, branch: branch, file: result.file}) }}#L{{ result.line }}">{{ result.file }}</a></td>
|
||||
<td><i class="icon-file icon-spaced"></i> <a href="{{ path('blob', {repo: repo, branch: branch, file: result.file, commitishPath: branch ~ '/' ~ result.file}) }}#L{{ result.line }}">{{ result.file }}</a></td>
|
||||
<td>{{ result.match }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
{% block title %}GitList{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
{% include 'breadcrumb.twig' with {breadcrumbs: [{dir: 'Commits search results', path:''}]} %}
|
||||
{% include 'breadcrumb.twig' with {breadcrumbs: [{dir: 'Commits search results for: ' ~ query, path:''}]} %}
|
||||
|
||||
{% include 'commits_list.twig' %}
|
||||
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
{% endblock %}
|
||||
{% endembed %}
|
||||
|
||||
{% if files is not empty %}
|
||||
<table class="tree">
|
||||
<thead>
|
||||
<tr>
|
||||
@@ -32,7 +33,7 @@
|
||||
{% if not parent %}
|
||||
<a href="{{ path('branch', {repo: repo, branch: branch}) }}">..</a>
|
||||
{% else %}
|
||||
<a href="{{ path('tree', {repo: repo, branch: branch, tree: parent}) }}">..</a>
|
||||
<a href="{{ path('tree', {repo: repo, commitishPath: branch ~ '/' ~ parent}) }}">..</a>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td></td>
|
||||
@@ -43,9 +44,9 @@
|
||||
<tr>
|
||||
<td><i class="{{ file.type == "folder" or file.type == "symlink" ? "icon-folder-open" : "icon-file" }} icon-spaced"></i> <a href="
|
||||
{%- if file.type == "folder" or file.type == "symlink" -%}
|
||||
{{ path('tree', {repo: repo, branch: branch, tree: path ~ (file.type == "symlink" ? file.path : file.name)}) }}
|
||||
{{ path('tree', {repo: repo, commitishPath: branch ~ '/' ~ path ~ (file.type == "symlink" ? file.path : file.name)}) }}
|
||||
{%- else -%}
|
||||
{{ path('blob', {repo: repo, branch: branch, file: path ~ (file.type == "symlink" ? file.path : file.name)}) }}
|
||||
{{ path('blob', {repo: repo, commitishPath: branch ~ '/' ~ path ~ (file.type == "symlink" ? file.path : file.name)}) }}
|
||||
{%- endif -%}
|
||||
">{{ file.name }}</a></td>
|
||||
<td>{{ file.mode }}</td>
|
||||
@@ -54,12 +55,15 @@
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% else %}
|
||||
<p>This repository is empty.</p>
|
||||
{% endif %}
|
||||
{% if readme is defined and readme is not empty %}
|
||||
<div class="readme-view">
|
||||
<div class="readme-header">
|
||||
<div class="md-header">
|
||||
<div class="meta">{{ readme.filename }}</div>
|
||||
</div>
|
||||
<div id="readme-content">{{ readme.content }}</div>
|
||||
<div id="md-content">{{ readme.content }}</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
|
||||
File diff suppressed because one or more lines are too long
BIN
web/img/favicon.png
Normal file
BIN
web/img/favicon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 884 B |
@@ -20,22 +20,23 @@ $(function () {
|
||||
});
|
||||
}
|
||||
|
||||
if ($('#readme-content').length) {
|
||||
if ($('#md-content').length) {
|
||||
var converter = new Showdown.converter();
|
||||
$('#readme-content').html(converter.makeHtml($('#readme-content').text()));
|
||||
$('#md-content').html(converter.makeHtml($('#md-content').text()));
|
||||
}
|
||||
|
||||
function paginate() {
|
||||
var $pager = $('.pager');
|
||||
|
||||
$pager.find('.next a').one('click', function (e) {
|
||||
e.preventDefault();
|
||||
$(this).css('pointer-events', 'none');
|
||||
$.get(this.href, function (html) {
|
||||
$pager.after(html);
|
||||
$pager.remove();
|
||||
paginate();
|
||||
});
|
||||
});
|
||||
|
||||
$pager.find('.previous').remove();
|
||||
}
|
||||
paginate();
|
||||
|
||||
581
web/js/networkGraph.js
Normal file
581
web/js/networkGraph.js
Normal file
@@ -0,0 +1,581 @@
|
||||
/**
|
||||
* Network Graph JS
|
||||
* This File is a part of the GitList Project at http://gitlist.org
|
||||
*
|
||||
* @license http://www.opensource.org/licenses/bsd-license.php
|
||||
* @author Lukas Domnick <lukx@lukx.de> http://github.com/lukx
|
||||
*/
|
||||
|
||||
( function( $ ){
|
||||
// global config
|
||||
var cfg = {
|
||||
laneColors: ['#ff0000', '#0000FF', '#00FFFF', '#00FF00', '#FFFF00', '#ff00ff'],
|
||||
laneHeight: 20,
|
||||
columnWidth: 42,
|
||||
dotRadius: 3
|
||||
};
|
||||
|
||||
// Define the jQuery Plugins
|
||||
|
||||
/**
|
||||
* DragScrollr is a custom made x/y-Drag Scroll Plugin for Gitlist
|
||||
*
|
||||
* TODO: Make this touch-scrollable
|
||||
*/
|
||||
$.fn.dragScrollr = function() {
|
||||
var lastX,
|
||||
lastY,
|
||||
hotZone = 200,
|
||||
container = this.first(),
|
||||
domElement = container[0]; // so basically container without the jQuery stuff
|
||||
|
||||
function handleMouseDown( evt ) {
|
||||
container.on('mousemove', handleMouseMove);
|
||||
container.on('mouseup', handleMouseUp);
|
||||
container.on('mouseleave', handleMouseUp);
|
||||
lastX = evt.pageX;
|
||||
lastY = evt.pageY;
|
||||
}
|
||||
|
||||
function handleMouseMove(evt) {
|
||||
evt.preventDefault();
|
||||
|
||||
// save the last scroll position to figure out whether the scroll event has entered the hot zone
|
||||
var lastScrollLeft = domElement.scrollLeft;
|
||||
domElement.scrollLeft = domElement.scrollLeft + lastX - evt.pageX;
|
||||
domElement.scrollTop = domElement.scrollTop + lastY - evt.pageY;
|
||||
|
||||
if( lastScrollLeft > hotZone && domElement.scrollLeft <= hotZone ) {
|
||||
container.trigger('enterHotZone');
|
||||
}
|
||||
|
||||
// when we move into the hot zone
|
||||
|
||||
lastX = evt.pageX;
|
||||
lastY = evt.pageY;
|
||||
}
|
||||
|
||||
function handleMouseUp(evt) {
|
||||
container.off('mousemove', handleMouseMove)
|
||||
.off('mouseup', handleMouseUp)
|
||||
.off('mouseleave', handleMouseUp);
|
||||
}
|
||||
|
||||
// now bind the initial event
|
||||
container.on('mousedown', handleMouseDown);
|
||||
|
||||
// return this instead of container, because of the .first() we applied - remember?
|
||||
return this;
|
||||
};
|
||||
|
||||
function graphLaneManager() {
|
||||
var that = {},
|
||||
occupiedLanes = [];
|
||||
|
||||
// "private" methods
|
||||
function findLaneNumberFor( commit ) {
|
||||
|
||||
if( commit.lane ) {
|
||||
// oh? we've already got a lane?
|
||||
return commit.lane.number;
|
||||
}
|
||||
|
||||
// find out which lane may draw our dot on. Start with a free one
|
||||
var laneNumber = findFreeLane();
|
||||
|
||||
// if the child is a merge, we need to figure out which lane we may render this commit on.
|
||||
// Rules are simple: A "parent" by the same author as the merge may render on the same line as the child
|
||||
// others take the next free lane.
|
||||
// furthermore, commits in a linear line of events may stay on the same lane, too
|
||||
if( commit.children.length > 0) {
|
||||
if( !commit.children[0].isMerge // linear ...
|
||||
|| ( commit.children[0].isMerge && commit.children[0].author.email === commit.author.email ) // same author
|
||||
) {
|
||||
laneNumber = commit.children[0].lane.number;
|
||||
}
|
||||
}
|
||||
|
||||
return laneNumber;
|
||||
}
|
||||
|
||||
function findFreeLane() {
|
||||
var i = 0;
|
||||
|
||||
while( true ) {
|
||||
// if an array index is not yet defined or set to false, the lane with that number is free.
|
||||
if( !occupiedLanes[i] ) {
|
||||
return i;
|
||||
}
|
||||
i ++;
|
||||
}
|
||||
}
|
||||
|
||||
that.occupy = function( lane ) {
|
||||
// make sure we work with lane numbers here
|
||||
if( typeof lane === 'object' ) {
|
||||
lane = lane.number;
|
||||
}
|
||||
|
||||
occupiedLanes[lane] = true;
|
||||
};
|
||||
|
||||
that.free = function( lane ) {
|
||||
// make sure we work with lane numbers here
|
||||
if( typeof lane === 'object' ) {
|
||||
lane = lane.number;
|
||||
}
|
||||
|
||||
occupiedLanes[lane] = false;
|
||||
};
|
||||
|
||||
that.getLaneForCommit = function( commit ) {
|
||||
// does this commit have a lane already?
|
||||
if( commit.lane ) return commit.lane;
|
||||
|
||||
var laneNumber = findLaneNumberFor( commit );
|
||||
return that.getLane( laneNumber );
|
||||
};
|
||||
|
||||
that.getLane = function(laneNumber) {
|
||||
return {
|
||||
'number': laneNumber,
|
||||
'centerY': ( laneNumber * cfg.laneHeight ) + (cfg.laneHeight/2),
|
||||
'color': cfg.laneColors[ laneNumber % cfg.laneColors.length ]
|
||||
};
|
||||
};
|
||||
|
||||
return that;
|
||||
}
|
||||
|
||||
function commitDetailOverlay( ) {
|
||||
var that = {},
|
||||
el = $('<div class="network-commit-overlay"></div>'),
|
||||
imageDisplay = $('<img/>').appendTo(el),
|
||||
messageDisplay = $('<h4></h4>').appendTo(el),
|
||||
metaDisplay = $('<p></p>').appendTo(el),
|
||||
authorDisplay = $('<a rel="author"></a>').appendTo(metaDisplay),
|
||||
dateDisplay = $('<span></span>').appendTo(metaDisplay),
|
||||
|
||||
commit;
|
||||
|
||||
el.hide();
|
||||
/**
|
||||
* Pads an input number with one leading '0' if needed, and assure it's a string
|
||||
*
|
||||
* @param input Number
|
||||
* @returns String
|
||||
*/
|
||||
function twoDigits( input ) {
|
||||
if( input < 10 ) {
|
||||
return '0' + input;
|
||||
}
|
||||
|
||||
return '' + input;
|
||||
}
|
||||
|
||||
/**
|
||||
* Transform a JS Native Date Object to a string, maintaining the same format given in the commit_list view
|
||||
* 'd/m/Y \\a\\t H:i:s'
|
||||
*
|
||||
* @param date Date
|
||||
* @returns String
|
||||
*/
|
||||
function getDateString( date ) {
|
||||
return twoDigits( date.getDate() ) + '/'
|
||||
+ twoDigits( date.getMonth() ) + '/'
|
||||
+ date.getFullYear() + ' at '
|
||||
+ twoDigits(date.getHours()) + ':'
|
||||
+ twoDigits(date.getMinutes()) + ':'
|
||||
+ twoDigits(date.getSeconds());
|
||||
}
|
||||
|
||||
/**
|
||||
* update the author view
|
||||
*
|
||||
* @param author
|
||||
*/
|
||||
function setAuthor( author ) {
|
||||
authorDisplay.html(author.name)
|
||||
.attr('href', 'mailto:' + author.email );
|
||||
|
||||
imageDisplay.attr('src', author.image );
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the commit that is being displayed in this detail overlay instance
|
||||
*
|
||||
* @param commit
|
||||
* @return that
|
||||
*/
|
||||
that.setCommit = function( commit ) {
|
||||
setAuthor( commit.author );
|
||||
dateDisplay.html( ' authored on ' + getDateString( commit.date ) );
|
||||
messageDisplay.html( commit.message );
|
||||
return that;
|
||||
};
|
||||
|
||||
// expose some jquery functions
|
||||
|
||||
that.show = function() {
|
||||
el.show();
|
||||
return that;
|
||||
};
|
||||
|
||||
that.hide = function() {
|
||||
el.hide();
|
||||
return that;
|
||||
};
|
||||
|
||||
that.appendTo = function(where) {
|
||||
el.appendTo(where);
|
||||
|
||||
return that;
|
||||
};
|
||||
|
||||
that.positionTo = function( x, y ) {
|
||||
el.css('left', x + 'px');
|
||||
el.css('top', y + 'px');
|
||||
};
|
||||
|
||||
that.outerWidth = function( ) {
|
||||
return el.outerWidth.apply(el, arguments);
|
||||
};
|
||||
|
||||
|
||||
return that;
|
||||
}
|
||||
|
||||
function commitDataRetriever( startPage, callback ) {
|
||||
var that = {},
|
||||
nextPage = startPage,
|
||||
isLoading = false,
|
||||
indicatorElements;
|
||||
|
||||
that.updateIndicators = function() {
|
||||
if( isLoading ) {
|
||||
$(indicatorElements).addClass('loading-commits');
|
||||
} else {
|
||||
$(indicatorElements).removeClass('loading-commits');
|
||||
}
|
||||
};
|
||||
|
||||
that.bindIndicator = function( el ) {
|
||||
if( !indicatorElements ) {
|
||||
indicatorElements = $(el);
|
||||
} else {
|
||||
indicatorElements = indicatorElements.add(el);
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
that.unbindIndicator = function( el ) {
|
||||
indicatorElements.not( el );
|
||||
};
|
||||
|
||||
function handleNetworkDataLoaded(data) {
|
||||
isLoading = false;
|
||||
that.updateIndicators();
|
||||
nextPage = data.nextPage;
|
||||
|
||||
if( !data.commits || data.commits.length === 0 ) {
|
||||
callback( null );
|
||||
}
|
||||
|
||||
callback(data.commits);
|
||||
}
|
||||
|
||||
function handleNetworkDataError() {
|
||||
throw "Network Data Error while retrieving Commits";
|
||||
}
|
||||
|
||||
that.retrieve = function() {
|
||||
|
||||
if( !nextPage ) {
|
||||
callback( null );
|
||||
return;
|
||||
}
|
||||
|
||||
isLoading = true;
|
||||
that.updateIndicators();
|
||||
$.ajax({
|
||||
dataType: "json",
|
||||
url: nextPage,
|
||||
success: handleNetworkDataLoaded,
|
||||
error: handleNetworkDataError
|
||||
});
|
||||
};
|
||||
|
||||
that.hasMore = function () {
|
||||
return ( !!nextPage );
|
||||
};
|
||||
|
||||
return that;
|
||||
}
|
||||
|
||||
|
||||
// the $(document).ready starting point
|
||||
$( function() {
|
||||
|
||||
// initialise network graph only when there is one network graph container on the page
|
||||
if( $('div.network-graph').length !== 1 ) {
|
||||
return;
|
||||
}
|
||||
|
||||
var
|
||||
// the element into which we will render our graph
|
||||
commitsGraph = $('div.network-graph').first(),
|
||||
laneManager = graphLaneManager(),
|
||||
dataRetriever = commitDataRetriever( commitsGraph.data('source'), handleCommitsRetrieved ),
|
||||
paper = Raphael( commitsGraph[0], commitsGraph.width(), commitsGraph.height()),
|
||||
usedColumns = 0,
|
||||
detailOverlay = commitDetailOverlay();
|
||||
|
||||
dataRetriever.bindIndicator( commitsGraph.parent('.network-view') );
|
||||
detailOverlay.appendTo( commitsGraph );
|
||||
|
||||
|
||||
function handleEnterHotZone() {
|
||||
dataRetriever.retrieve();
|
||||
}
|
||||
|
||||
function handleCommitsRetrieved( commits ) {
|
||||
|
||||
// no commits or empty commits array? Well, we can't draw a graph of that
|
||||
if( commits === null ) {
|
||||
handleNoAvailableData();
|
||||
return;
|
||||
}
|
||||
|
||||
prepareCommits( commits );
|
||||
renderCommits( commits );
|
||||
}
|
||||
|
||||
function handleNoAvailableData() {
|
||||
console.log('No Data available');
|
||||
}
|
||||
|
||||
var awaitedParents = {};
|
||||
|
||||
function prepareCommits( commits ) {
|
||||
$.each( commits, function ( index, commit) {
|
||||
prepareCommit( commit );
|
||||
});
|
||||
}
|
||||
|
||||
function prepareCommit( commit ) {
|
||||
// make "date" an actual JS Date object
|
||||
commit.date = new Date(commit.date*1000);
|
||||
|
||||
// the parents will be filled once they have become prepared
|
||||
commit.parents = [];
|
||||
|
||||
// we will want to store this commit's children
|
||||
commit.children = getChildrenFor( commit );
|
||||
|
||||
commit.isFork = ( commit.children.length > 1 );
|
||||
commit.isMerge = ( commit.parentsHash.length > 1 );
|
||||
|
||||
// after a fork, the occupied lanes must be cleaned up. The children used some lanes we no longer occupy
|
||||
if ( commit.isFork === true ) {
|
||||
$.each( commit.children, function( key, thisChild ) {
|
||||
// free this lane
|
||||
laneManager.occupy( thisChild.lane );
|
||||
});
|
||||
}
|
||||
|
||||
commit.lane = laneManager.getLaneForCommit( commit );
|
||||
|
||||
// now the lane we chose must be marked occupied again.
|
||||
laneManager.occupy( commit.lane );
|
||||
|
||||
registerAwaitedParentsFor( commit );
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a new childCommit to the dictionary of awaited parents
|
||||
*
|
||||
* @param commit who is waiting?
|
||||
*/
|
||||
function registerAwaitedParentsFor( commit ) {
|
||||
// This commit's parents are not yet known in our little world, as we are rendering following the time line.
|
||||
// Therefore we are registering this commit as "waiting" for each of the parent hashes
|
||||
$.each( commit.parentsHash, function( key, thisParentHash ) {
|
||||
// If awaitedParents does not already have a key for thisParent's hash, initialise as array
|
||||
if( !awaitedParents.hasOwnProperty(thisParentHash) ) {
|
||||
awaitedParents[thisParentHash] = [ commit ];
|
||||
} else {
|
||||
awaitedParents[ thisParentHash ].push( commit );
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function getChildrenFor( commit ) {
|
||||
var children = [];
|
||||
|
||||
if( awaitedParents.hasOwnProperty( commit.hash )) {
|
||||
// there are child commits waiting
|
||||
children = awaitedParents[ commit.hash ];
|
||||
|
||||
// let the children know their parent objects
|
||||
$.each( children, function(key, thisChild ) {
|
||||
thisChild.parents.push( commit );
|
||||
});
|
||||
|
||||
// remove this item from parentsBeingWaitedFor
|
||||
delete awaitedParents[ commit.hash ];
|
||||
}
|
||||
|
||||
return children;
|
||||
}
|
||||
|
||||
var lastRenderedDate = new Date(0);
|
||||
function renderCommits( commits ) {
|
||||
|
||||
var neededWidth = ((usedColumns + Object.keys(commits).length) * cfg.columnWidth);
|
||||
|
||||
if ( neededWidth > paper.width ) {
|
||||
extendPaper( neededWidth, paper.height );
|
||||
} else if( dataRetriever.hasMore() ) {
|
||||
// this is the case when we have not loaded enough commits to fill the paper yet. Get some more then...
|
||||
dataRetriever.retrieve();
|
||||
}
|
||||
|
||||
$.each( commits, function ( index, commit) {
|
||||
if( lastRenderedDate.getYear() !== commit.date.getYear()
|
||||
|| lastRenderedDate.getMonth() !== commit.date.getMonth()
|
||||
|| lastRenderedDate.getDate() !== commit.date.getDate() ) {
|
||||
// TODO: If desired, one could add a time scale on top, maybe.
|
||||
}
|
||||
renderCommit(commit);
|
||||
});
|
||||
}
|
||||
|
||||
function renderCommit( commit ) {
|
||||
// find the column this dot is drawn on
|
||||
usedColumns++;
|
||||
commit.column = usedColumns;
|
||||
|
||||
commit.dot = paper.circle( getXPositionForColumnNumber(commit.column), commit.lane.centerY, cfg.dotRadius );
|
||||
commit.dot.attr({
|
||||
fill: commit.lane.color,
|
||||
stroke: 'none',
|
||||
cursor: 'pointer'
|
||||
})
|
||||
.data('commit', commit)
|
||||
.mouseover( handleCommitMouseover )
|
||||
.mouseout( handleCommitMouseout )
|
||||
.click( handleCommitClick );
|
||||
|
||||
// maybe we have not enough space for the lane yet
|
||||
if( commit.lane.centerY + cfg.laneHeight > paper.height ) {
|
||||
extendPaper( paper.width, commit.lane.centerY + cfg.laneHeight )
|
||||
}
|
||||
|
||||
$.each( commit.children, function ( idx, thisChild ) {
|
||||
|
||||
// if there is one child only, stay on the commit's lane as long as possible when connecting the dots.
|
||||
// but if there is more than one child, switch to the child's lane ASAP.
|
||||
// this is to display merges and forks where they happen (ie. at a commit node/ a dot), rather than
|
||||
// connecting from a line.
|
||||
// So: commit.isFork decides whether or not we must switch lanes early
|
||||
|
||||
connectDots( commit, thisChild, commit.isFork );
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param firstCommit
|
||||
* @param secondCommit
|
||||
* @param switchLanesEarly (boolean): Move the line to the secondCommit's lane ASAP? Defaults to false
|
||||
*/
|
||||
function connectDots( firstCommit, secondCommit, switchLanesEarly ) {
|
||||
// default value for switchLanesEarly
|
||||
switchLanesEarly = switchLanesEarly || false;
|
||||
|
||||
var lineLane = switchLanesEarly ? secondCommit.lane : firstCommit.lane;
|
||||
|
||||
// the connection has 4 stops, resulting in the following 3 segments:
|
||||
// - from the x/y center of firstCommit.dot to the rightmost end (x) of the commit's column, with y=lineLane
|
||||
// - from the rightmost end of firstCommit's column, to the leftmost end of secondCommit's column
|
||||
// - from the leftmost end of secondCommit's column (y=lineLane) to the x/y center of secondCommit
|
||||
|
||||
paper.path(
|
||||
getSvgLineString(
|
||||
[firstCommit.dot.attr('cx'), firstCommit.dot.attr('cy')],
|
||||
[firstCommit.dot.attr('cx') + (cfg.columnWidth/2), lineLane.centerY],
|
||||
[secondCommit.dot.attr('cx') - (cfg.columnWidth/2), lineLane.centerY],
|
||||
[secondCommit.dot.attr('cx'), secondCommit.dot.attr('cy')]
|
||||
)
|
||||
).attr({ "stroke": lineLane.color, "stroke-width": 2 }).toBack();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// set together a path string from any amount of arguments
|
||||
// each argument is an array of [x, y] within the paper's coordinate system
|
||||
function getSvgLineString( ) {
|
||||
if (arguments.length < 2) return;
|
||||
|
||||
var svgString = 'M' + arguments[0][0] + ' ' + arguments[0][1];
|
||||
|
||||
for (var i = 1, j = arguments.length; i < j; i++){
|
||||
svgString += 'L' + arguments[i][0] + ' ' + arguments[i][1];
|
||||
}
|
||||
|
||||
return svgString;
|
||||
}
|
||||
|
||||
function handleCommitMouseover(evt) {
|
||||
detailOverlay.setCommit( this.data('commit'))
|
||||
.show();
|
||||
|
||||
var xPos = evt.pageX - commitsGraph.offset().left + commitsGraph.scrollLeft() - (detailOverlay.outerWidth()/2);
|
||||
// check that x doesn't run out the viewport
|
||||
xPos = Math.max( xPos, commitsGraph.scrollLeft() + 10);
|
||||
xPos = Math.min( xPos, commitsGraph.scrollLeft() + commitsGraph.width() - detailOverlay.outerWidth() - 10);
|
||||
|
||||
detailOverlay.positionTo( xPos,
|
||||
evt.pageY - commitsGraph.offset().top + commitsGraph.scrollTop() + 10);
|
||||
}
|
||||
|
||||
function handleCommitMouseout(evt) {
|
||||
detailOverlay.hide();
|
||||
}
|
||||
|
||||
function handleCommitClick( evt ) {
|
||||
window.open( this.data('commit').details );
|
||||
}
|
||||
|
||||
function getXPositionForColumnNumber( columnNumber ) {
|
||||
// we want the column's center point
|
||||
return ( paper.width - ( columnNumber * cfg.columnWidth ) + (cfg.columnWidth / 2 ));
|
||||
}
|
||||
|
||||
function extendPaper( newWidth, newHeight ) {
|
||||
var deltaX = newWidth - paper.width;
|
||||
|
||||
paper.setSize( newWidth, newHeight );
|
||||
// fixup parent's scroll position
|
||||
paper.canvas.parentNode.scrollLeft = paper.canvas.parentNode.scrollLeft + deltaX;
|
||||
|
||||
// now fixup the x positions of existing circles and lines
|
||||
paper.forEach( function( el ) {
|
||||
|
||||
if( el.type === "circle" ) {
|
||||
el.attr('cx', el.attr('cx') + deltaX);
|
||||
} else if ( el.type === "path") {
|
||||
var newXTranslation = el.data('currentXTranslation') || 0;
|
||||
newXTranslation += deltaX;
|
||||
el.transform( 't' + newXTranslation + ' 0' );
|
||||
el.data('currentXTranslation', newXTranslation);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
commitsGraph.dragScrollr();
|
||||
commitsGraph.on('enterHotZone', handleEnterHotZone);
|
||||
// load initial data
|
||||
dataRetriever.retrieve( );
|
||||
});
|
||||
}( jQuery ));
|
||||
10
web/js/raphael.js
Normal file
10
web/js/raphael.js
Normal file
File diff suppressed because one or more lines are too long
1
web/less/bootstrap.less
vendored
1
web/less/bootstrap.less
vendored
@@ -26,6 +26,7 @@
|
||||
@import "forms.less";
|
||||
@import "tables.less";
|
||||
@import "files.less";
|
||||
@import "network.less";
|
||||
|
||||
// Components: common
|
||||
@import "sprites.less";
|
||||
|
||||
@@ -161,30 +161,33 @@
|
||||
}
|
||||
|
||||
.readme-view {
|
||||
border: 1px solid @treeHeaderBorder;
|
||||
}
|
||||
|
||||
.md-view {
|
||||
width: 100%;
|
||||
margin-bottom: @baseLineHeight;
|
||||
border: 1px solid @treeHeaderBorder;
|
||||
}
|
||||
|
||||
.readme-header {
|
||||
padding: 8px;
|
||||
line-height: @baseLineHeight;
|
||||
text-align: left;
|
||||
vertical-align: bottom;
|
||||
#gradient > .vertical(@treeHeaderHighlight, @treeHeader);
|
||||
border-bottom: 1px solid lighten(@treeHeaderBorder, 5%);
|
||||
font-weight: bold;
|
||||
color: @gray;
|
||||
text-shadow: 1px 1px 1px rgba(255,255,255,1);
|
||||
height:28px;
|
||||
.meta {
|
||||
float: left;
|
||||
padding: 4px 0;
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
#readme-content {
|
||||
padding: 30px;
|
||||
color: @black;
|
||||
.md-header {
|
||||
padding: 8px;
|
||||
line-height: @baseLineHeight;
|
||||
text-align: left;
|
||||
vertical-align: bottom;
|
||||
#gradient > .vertical(@treeHeaderHighlight, @treeHeader);
|
||||
border-bottom: 1px solid lighten(@treeHeaderBorder, 5%);
|
||||
font-weight: bold;
|
||||
color: @gray;
|
||||
text-shadow: 1px 1px 1px rgba(255,255,255,1);
|
||||
height:28px;
|
||||
.meta {
|
||||
float: left;
|
||||
padding: 4px 0;
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
#md-content {
|
||||
padding: 30px;
|
||||
color: @black;
|
||||
}
|
||||
94
web/less/network.less
Normal file
94
web/less/network.less
Normal file
@@ -0,0 +1,94 @@
|
||||
.network-view {
|
||||
width: 100%;
|
||||
margin-bottom: @baseLineHeight;
|
||||
border: 1px solid @treeHeaderBorder;
|
||||
position: relative;
|
||||
|
||||
&.loading-commits {
|
||||
|
||||
&:before {
|
||||
content: "";
|
||||
display: block;
|
||||
height: 100px;
|
||||
width: 200px;
|
||||
box-sizing: border-box;
|
||||
padding: 10px;
|
||||
font-weight: bold;
|
||||
position: absolute;
|
||||
|
||||
left: 50%;
|
||||
top: 50%;
|
||||
margin-left: -100px;
|
||||
margin-top: -50px;
|
||||
|
||||
color: #555;
|
||||
background: rgba(255,255,255,.8) url("@{ajaxLoaderPath}") no-repeat center;
|
||||
z-index: 2000;
|
||||
border: 1px solid #eee;
|
||||
border-radius: 8px;
|
||||
box-shadow: 2px 2px 2px #eee;
|
||||
}
|
||||
|
||||
.network-header .meta:after {
|
||||
content: " - Loading";
|
||||
}
|
||||
}
|
||||
|
||||
.network-header {
|
||||
padding: 8px;
|
||||
line-height: @baseLineHeight;
|
||||
text-align: left;
|
||||
vertical-align: bottom;
|
||||
#gradient > .vertical(@treeHeaderHighlight, @treeHeader);
|
||||
border-bottom: 1px solid lighten(@treeHeaderBorder, 5%);
|
||||
font-weight: bold;
|
||||
color: @gray;
|
||||
text-shadow: 1px 1px 1px rgba(255, 255, 255, 1);
|
||||
height: 28px;
|
||||
|
||||
.meta {
|
||||
float: left;
|
||||
padding: 4px 0;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
}
|
||||
pre {
|
||||
margin: 0;
|
||||
padding: 12px;
|
||||
border: none;
|
||||
}
|
||||
.network-graph {
|
||||
background-color: #f5f5f5;
|
||||
height: 400px;
|
||||
overflow: hidden;
|
||||
cursor: move;
|
||||
position: relative;
|
||||
|
||||
.network-commit-overlay {
|
||||
position: absolute;
|
||||
width: 400px;
|
||||
top: 0;
|
||||
left: 0;
|
||||
padding: 8px;
|
||||
margin: 0 0 @baseLineHeight;
|
||||
list-style: none;
|
||||
#gradient > .vertical(@white, #f5f5f5);
|
||||
border: 1px solid #ddd;
|
||||
|
||||
.border-radius(3px);
|
||||
.box-shadow(~"0 1px 0 rgba(255,255,255,0.4), 0 0 10px rgba(0,0,0,0.1)");
|
||||
|
||||
img {
|
||||
float: left;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
&>h4,
|
||||
&>p {
|
||||
padding-left: 48px;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -131,6 +131,7 @@
|
||||
@iconSpritePath: "../img/glyphicons-halflings.png";
|
||||
@iconWhiteSpritePath: "../img/glyphicons-halflings-white.png";
|
||||
@rssIconPath: "../img/feed.png";
|
||||
@ajaxLoaderPath: "../img/ajax-loader.gif";
|
||||
|
||||
// Input placeholder text color
|
||||
// -------------------------
|
||||
|
||||
Reference in New Issue
Block a user