aboutsummaryrefslogtreecommitdiffstats
path: root/git_deps
diff options
context:
space:
mode:
authorAdam Spiers <git@adamspiers.org>2016-06-11 22:20:04 +0100
committerAdam Spiers <git@adamspiers.org>2018-05-15 13:42:16 +0100
commit2c9d23b0291157eb1096384ff76e0122747b9bdf (patch)
tree524c7b479b65a478c998c28475d52e636b919200 /git_deps
parent9a741f07167dcb6cc81a8f87036d1ea75c4270d3 (diff)
downloadgit-deps-2c9d23b0291157eb1096384ff76e0122747b9bdf.tar.gz
convert into a proper Python module
Sem-Ver: api-break
Diffstat (limited to 'git_deps')
-rw-r--r--git_deps/__init__.py6
-rwxr-xr-xgit_deps/cli.py148
-rw-r--r--git_deps/detector.py332
-rw-r--r--git_deps/errors.py6
-rw-r--r--git_deps/gitutils.py68
-rwxr-xr-xgit_deps/handler.py38
-rw-r--r--git_deps/html/.gitignore2
-rw-r--r--git_deps/html/css/animate.css3158
-rw-r--r--git_deps/html/css/git-deps-tips.css79
-rw-r--r--git_deps/html/css/git-deps.css93
-rw-r--r--git_deps/html/git-deps.html40
-rw-r--r--git_deps/html/js/.gitignore1
-rw-r--r--git_deps/html/js/fullscreen.js48
-rw-r--r--git_deps/html/js/git-deps-data.coffee108
-rw-r--r--git_deps/html/js/git-deps-graph.coffee595
-rw-r--r--git_deps/html/js/git-deps-layout.coffee253
-rw-r--r--git_deps/html/js/git-deps-noty.coffee32
-rw-r--r--git_deps/html/package.json54
-rw-r--r--git_deps/html/test.json442
-rw-r--r--git_deps/html/tip-template.html11
-rw-r--r--git_deps/listener/__init__.py0
-rw-r--r--git_deps/listener/base.py35
-rw-r--r--git_deps/listener/cli.py56
-rw-r--r--git_deps/listener/json.py86
-rw-r--r--git_deps/server.py122
-rw-r--r--git_deps/utils.py8
26 files changed, 5821 insertions, 0 deletions
diff --git a/git_deps/__init__.py b/git_deps/__init__.py
new file mode 100644
index 0000000..896994c
--- /dev/null
+++ b/git_deps/__init__.py
@@ -0,0 +1,6 @@
+import pkg_resources
+
+try:
+ __version__ = pkg_resources.get_distribution(__name__).version
+except:
+ __version__ = 'unknown'
diff --git a/git_deps/cli.py b/git_deps/cli.py
new file mode 100755
index 0000000..0fa06d1
--- /dev/null
+++ b/git_deps/cli.py
@@ -0,0 +1,148 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+"""
+git-deps - automatically detect dependencies between git commits
+Copyright (C) 2013 Adam Spiers <git@adamspiers.org>
+
+The software in this repository is free software: you can redistribute
+it and/or modify it under the terms of the GNU General Public License
+as published by the Free Software Foundation, either version 2 of the
+License, or (at your option) any later version.
+
+This software is distributed in the hope that it will be useful, but
+WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program. If not, see <http://www.gnu.org/licenses/>.
+"""
+
+from __future__ import print_function
+
+import argparse
+import json
+import sys
+
+from git_deps import __version__
+from git_deps.detector import DependencyDetector
+from git_deps.errors import InvalidCommitish
+from git_deps.gitutils import GitUtils
+from git_deps.listener.json import JSONDependencyListener
+from git_deps.listener.cli import CLIDependencyListener
+from git_deps.server import serve
+from git_deps.utils import abort
+
+__author__ = "Adam Spiers"
+__copyright__ = "Adam Spiers"
+__license__ = "GPL-2+"
+
+
+def parse_args():
+ parser = argparse.ArgumentParser(
+ description='Auto-detects commits on which the given '
+ 'commit(s) depend.',
+ usage='%(prog)s [options] COMMIT-ISH [COMMIT-ISH...]',
+ add_help=False
+ )
+ parser.add_argument('-h', '--help', action='help',
+ help='Show this help message and exit')
+ parser.add_argument('-v', '--version', action='version',
+ version='git-deps {ver}'.format(ver=__version__))
+ parser.add_argument('-l', '--log', dest='log', action='store_true',
+ help='Show commit logs for calculated dependencies')
+ parser.add_argument('-j', '--json', dest='json', action='store_true',
+ help='Output dependencies as JSON')
+ parser.add_argument('-s', '--serve', dest='serve', action='store_true',
+ help='Run a web server for visualizing the '
+ 'dependency graph')
+ parser.add_argument('-b', '--bind-ip', dest='bindaddr', type=str,
+ metavar='IP', default='127.0.0.1',
+ help='IP address for webserver to bind to [%(default)s]')
+ parser.add_argument('-p', '--port', dest='port', type=int, metavar='PORT',
+ default=5000,
+ help='Port number for webserver [%(default)s]')
+ parser.add_argument('-r', '--recurse', dest='recurse', action='store_true',
+ help='Follow dependencies recursively')
+ parser.add_argument('-e', '--exclude-commits', dest='exclude_commits',
+ action='append', metavar='COMMITISH',
+ help='Exclude commits which are ancestors of the '
+ 'given COMMITISH (can be repeated)')
+ parser.add_argument('-c', '--context-lines', dest='context_lines',
+ type=int, metavar='NUM', default=1,
+ help='Number of lines of diff context to use '
+ '[%(default)s]')
+ parser.add_argument('-d', '--debug', dest='debug', action='store_true',
+ help='Show debugging')
+
+ options, args = parser.parse_known_args()
+
+ # Are we potentially detecting dependencies for more than one commit?
+ # Even if we're not recursing, the user could specify multiple commits
+ # via CLI arguments.
+ options.multi = options.recurse
+
+ if options.serve:
+ if options.log:
+ parser.error('--log does not make sense in webserver mode.')
+ if options.json:
+ parser.error('--json does not make sense in webserver mode.')
+ if options.recurse:
+ parser.error('--recurse does not make sense in webserver mode.')
+ if len(args) > 0:
+ parser.error('Specifying commit-ishs does not make sense in '
+ 'webserver mode.')
+ else:
+ if len(args) == 0:
+ parser.error('You must specify at least one commit-ish.')
+
+ return options, args
+
+
+def cli(options, args):
+ detector = DependencyDetector(options)
+
+ if options.json:
+ listener = JSONDependencyListener(options)
+ else:
+ listener = CLIDependencyListener(options)
+
+ detector.add_listener(listener)
+
+ if len(args) > 1:
+ options.multi = True
+
+ for revspec in args:
+ revs = GitUtils.rev_list(revspec)
+ if len(revs) > 1:
+ options.multi = True
+
+ for rev in revs:
+ try:
+ detector.find_dependencies(rev)
+ except KeyboardInterrupt:
+ pass
+
+ if options.json:
+ print(json.dumps(listener.json(), sort_keys=True, indent=4))
+
+
+def main(args):
+ options, args = parse_args()
+ # rev_list = sys.stdin.readlines()
+
+ if options.serve:
+ serve(options)
+ else:
+ try:
+ cli(options, args)
+ except InvalidCommitish as e:
+ abort(e.message())
+
+
+def run():
+ main(sys.argv[1:])
+
+
+if __name__ == "__main__":
+ run()
diff --git a/git_deps/detector.py b/git_deps/detector.py
new file mode 100644
index 0000000..650c077
--- /dev/null
+++ b/git_deps/detector.py
@@ -0,0 +1,332 @@
+import logging
+import re
+import subprocess
+import sys
+
+import pygit2
+
+from git_deps.utils import abort
+from git_deps.listener.base import DependencyListener
+from git_deps.errors import InvalidCommitish
+
+
+class DependencyDetector(object):
+ """Class for automatically detecting dependencies between git commits.
+ A dependency is inferred by diffing the commit with each of its
+ parents, and for each resulting hunk, performing a blame to see
+ which commit was responsible for introducing the lines to which
+ the hunk was applied.
+
+ Dependencies can be traversed recursively, building a dependency
+ tree represented (conceptually) by a list of edges.
+ """
+
+ def __init__(self, options, repo_path=None, logger=None):
+ self.options = options
+
+ if logger is None:
+ self.logger = self.default_logger()
+
+ if repo_path is None:
+ try:
+ repo_path = pygit2.discover_repository('.')
+ except KeyError:
+ abort("Couldn't find a repository in the current directory.")
+
+ self.repo = pygit2.Repository(repo_path)
+
+ # Nested dict mapping dependents -> dependencies -> files
+ # causing that dependency -> numbers of lines within that file
+ # causing that dependency. The first two levels form edges in
+ # the dependency graph, and the latter two tell us what caused
+ # those edges.
+ self.dependencies = {}
+
+ # A TODO list (queue) and dict of dependencies which haven't
+ # yet been recursively followed. Only useful when recursing.
+ self.todo = []
+ self.todo_d = {}
+
+ # An ordered list and dict of commits whose dependencies we
+ # have already detected.
+ self.done = []
+ self.done_d = {}
+
+ # A cache mapping SHA1s to commit objects
+ self.commits = {}
+
+ # Memoization for branch_contains()
+ self.branch_contains_cache = {}
+
+ # Callbacks to be invoked when a new dependency has been
+ # discovered.
+ self.listeners = []
+
+ def add_listener(self, listener):
+ if not isinstance(listener, DependencyListener):
+ raise RuntimeError("Listener must be a DependencyListener")
+ self.listeners.append(listener)
+ listener.set_detector(self)
+
+ def notify_listeners(self, event, *args):
+ for listener in self.listeners:
+ fn = getattr(listener, event)
+ fn(*args)
+
+ def default_logger(self):
+ if not self.options.debug:
+ return logging.getLogger(self.__class__.__name__)
+
+ log_format = '%(asctime)-15s %(levelname)-6s %(message)s'
+ date_format = '%b %d %H:%M:%S'
+ formatter = logging.Formatter(fmt=log_format, datefmt=date_format)
+ handler = logging.StreamHandler(stream=sys.stdout)
+ handler.setFormatter(formatter)
+ # logger = logging.getLogger(__name__)
+ logger = logging.getLogger(self.__class__.__name__)
+ logger.setLevel(logging.DEBUG)
+ logger.addHandler(handler)
+ return logger
+
+ def seen_commit(self, rev):
+ return rev in self.commits
+
+ def get_commit(self, rev):
+ if rev in self.commits:
+ return self.commits[rev]
+
+ try:
+ self.commits[rev] = self.repo.revparse_single(rev)
+ except (KeyError, ValueError):
+ raise InvalidCommitish(rev)
+
+ return self.commits[rev]
+
+ def find_dependencies(self, dependent_rev, recurse=None):
+ """Find all dependencies of the given revision, recursively traversing
+ the dependency tree if requested.
+ """
+ if recurse is None:
+ recurse = self.options.recurse
+
+ try:
+ dependent = self.get_commit(dependent_rev)
+ except InvalidCommitish as e:
+ abort(e.message())
+
+ self.todo.append(dependent)
+ self.todo_d[dependent.hex] = True
+
+ while self.todo:
+ sha1s = [commit.hex[:8] for commit in self.todo]
+ self.logger.debug("TODO list: %s" % " ".join(sha1s))
+ dependent = self.todo.pop(0)
+ del self.todo_d[dependent.hex]
+ self.logger.debug("Processing %s from TODO list" %
+ dependent.hex[:8])
+ self.notify_listeners('new_commit', dependent)
+
+ for parent in dependent.parents:
+ self.find_dependencies_with_parent(dependent, parent)
+ self.done.append(dependent.hex)
+ self.done_d[dependent.hex] = True
+ self.logger.debug("Found all dependencies for %s" %
+ dependent.hex[:8])
+ # A commit won't have any dependencies if it only added new files
+ dependencies = self.dependencies.get(dependent.hex, {})
+ self.notify_listeners('dependent_done', dependent, dependencies)
+
+ self.notify_listeners('all_done')
+
+ def find_dependencies_with_parent(self, dependent, parent):
+ """Find all dependencies of the given revision caused by the given
+ parent commit. This will be called multiple times for merge
+ commits which have multiple parents.
+ """
+ self.logger.debug(" Finding dependencies of %s via parent %s" %
+ (dependent.hex[:8], parent.hex[:8]))
+ diff = self.repo.diff(parent, dependent,
+ context_lines=self.options.context_lines)
+ for patch in diff:
+ path = patch.delta.old_file.path
+ self.logger.debug(" Examining hunks in %s" % path)
+ for hunk in patch.hunks:
+ self.blame_hunk(dependent, parent, path, hunk)
+
+ def blame_hunk(self, dependent, parent, path, hunk):
+ """Run git blame on the parts of the hunk which exist in the older
+ commit in the diff. The commits generated by git blame are
+ the commits which the newer commit in the diff depends on,
+ because without the lines from those commits, the hunk would
+ not apply correctly.
+ """
+ first_line_num = hunk.old_start
+ line_range_before = "-%d,%d" % (hunk.old_start, hunk.old_lines)
+ line_range_after = "+%d,%d" % (hunk.new_start, hunk.new_lines)
+ self.logger.debug(" Blaming hunk %s @ %s" %
+ (line_range_before, parent.hex[:8]))
+
+ if not self.tree_lookup(path, parent):
+ # This is probably because dependent added a new directory
+ # which was not previously in the parent.
+ return
+
+ cmd = [
+ 'git', 'blame',
+ '--porcelain',
+ '-L', "%d,+%d" % (hunk.old_start, hunk.old_lines),
+ parent.hex, '--', path
+ ]
+ blame = subprocess.check_output(cmd)
+
+ dependent_sha1 = dependent.hex
+ if dependent_sha1 not in self.dependencies:
+ self.logger.debug(' New dependent: %s (%s)' %
+ (dependent_sha1[:8], self.oneline(dependent)))
+ self.dependencies[dependent_sha1] = {}
+ self.notify_listeners('new_dependent', dependent)
+
+ line_to_culprit = {}
+
+ for line in blame.split('\n'):
+ # self.logger.debug(' !' + line.rstrip())
+ m = re.match('^([0-9a-f]{40}) (\d+) (\d+)( \d+)?$', line)
+ if not m:
+ continue
+ dependency_sha1, orig_line_num, line_num = m.group(1, 2, 3)
+ line_num = int(line_num)
+ dependency = self.get_commit(dependency_sha1)
+ line_to_culprit[line_num] = dependency.hex
+
+ if self.is_excluded(dependency):
+ self.logger.debug(
+ ' Excluding dependency %s from line %s (%s)' %
+ (dependency_sha1[:8], line_num,
+ self.oneline(dependency)))
+ continue
+
+ if dependency_sha1 not in self.dependencies[dependent_sha1]:
+ if dependency_sha1 in self.todo_d:
+ self.logger.debug(
+ ' Dependency %s via line %s already in TODO' %
+ (dependency_sha1[:8], line_num,))
+ continue
+
+ if dependency_sha1 in self.done_d:
+ self.logger.debug(
+ ' Dependency %s via line %s already done' %
+ (dependency_sha1[:8], line_num,))
+ continue
+
+ self.logger.debug(
+ ' New dependency %s via line %s (%s)' %
+ (dependency_sha1[:8], line_num, self.oneline(dependency)))
+ self.dependencies[dependent_sha1][dependency_sha1] = {}
+ self.notify_listeners('new_commit', dependency)
+ self.notify_listeners('new_dependency',
+ dependent, dependency, path, line_num)
+ if dependency_sha1 not in self.dependencies:
+ if self.options.recurse:
+ self.todo.append(dependency)
+ self.todo_d[dependency.hex] = True
+ self.logger.debug(' added to TODO')
+
+ dep_sources = self.dependencies[dependent_sha1][dependency_sha1]
+
+ if path not in dep_sources:
+ dep_sources[path] = {}
+ self.notify_listeners('new_path',
+ dependent, dependency, path, line_num)
+
+ if line_num in dep_sources[path]:
+ abort("line %d already found when blaming %s:%s" %
+ (line_num, parent.hex[:8], path))
+
+ dep_sources[path][line_num] = True
+ self.notify_listeners('new_line',
+ dependent, dependency, path, line_num)
+
+ diff_format = ' |%8.8s %5s %s%s'
+ hunk_header = '@@ %s %s @@' % (line_range_before, line_range_after)
+ self.logger.debug(diff_format % ('--------', '-----', '', hunk_header))
+ line_num = hunk.old_start
+ for line in hunk.lines:
+ if "\n\\ No newline at end of file" == line.content.rstrip():
+ break
+ if line.origin == '+':
+ rev = ln = ''
+ else:
+ rev = line_to_culprit[line_num]
+ ln = line_num
+ line_num += 1
+ self.logger.debug(diff_format % (rev, ln, line.origin, line.content.rstrip()))
+
+ def oneline(self, commit):
+ return commit.message.split('\n', 1)[0]
+
+ def is_excluded(self, commit):
+ if self.options.exclude_commits is not None:
+ for exclude in self.options.exclude_commits:
+ if self.branch_contains(commit, exclude):
+ return True
+ return False
+
+ def branch_contains(self, commit, branch):
+ sha1 = commit.hex
+ branch_commit = self.get_commit(branch)
+ branch_sha1 = branch_commit.hex
+ self.logger.debug(" Does %s (%s) contain %s?" %
+ (branch, branch_sha1[:8], sha1[:8]))
+
+ if sha1 not in self.branch_contains_cache:
+ self.branch_contains_cache[sha1] = {}
+ if branch_sha1 in self.branch_contains_cache[sha1]:
+ memoized = self.branch_contains_cache[sha1][branch_sha1]
+ self.logger.debug(" %s (memoized)" % memoized)
+ return memoized
+
+ cmd = ['git', 'merge-base', sha1, branch_sha1]
+ # self.logger.debug(" ".join(cmd))
+ out = subprocess.check_output(cmd).strip()
+ self.logger.debug(" merge-base returned: %s" % out[:8])
+ result = out == sha1
+ self.logger.debug(" %s" % result)
+ self.branch_contains_cache[sha1][branch_sha1] = result
+ return result
+
+ def tree_lookup(self, target_path, commit):
+ """Navigate to the tree or blob object pointed to by the given target
+ path for the given commit. This is necessary because each git
+ tree only contains entries for the directory it refers to, not
+ recursively for all subdirectories.
+ """
+ segments = target_path.split("/")
+ tree_or_blob = commit.tree
+ path = ''
+ while segments:
+ dirent = segments.pop(0)
+ if isinstance(tree_or_blob, pygit2.Tree):
+ if dirent in tree_or_blob:
+ tree_or_blob = self.repo[tree_or_blob[dirent].oid]
+ # self.logger.debug('%s in %s' % (dirent, path))
+ if path:
+ path += '/'
+ path += dirent
+ else:
+ # This is probably because we were called on a
+ # commit whose parent added a new directory.
+ self.logger.debug(' %s not in %s in %s' %
+ (dirent, path, commit.hex[:8]))
+ return None
+ else:
+ self.logger.debug(' %s not a tree in %s' %
+ (tree_or_blob, commit.hex[:8]))
+ return None
+ return tree_or_blob
+
+ def edges(self):
+ return [
+ [(dependent, dependency)
+ for dependency in self.dependencies[dependent]]
+ for dependent in self.dependencies.keys()
+ ]
diff --git a/git_deps/errors.py b/git_deps/errors.py
new file mode 100644
index 0000000..1074624
--- /dev/null
+++ b/git_deps/errors.py
@@ -0,0 +1,6 @@
+class InvalidCommitish(StandardError):
+ def __init__(self, commitish):
+ self.commitish = commitish
+
+ def message(self):
+ return "Couldn't resolve commitish %s" % self.commitish
diff --git a/git_deps/gitutils.py b/git_deps/gitutils.py
new file mode 100644
index 0000000..f5d8281
--- /dev/null
+++ b/git_deps/gitutils.py
@@ -0,0 +1,68 @@
+import re
+import subprocess
+
+
+class GitUtils(object):
+ @classmethod
+ def abbreviate_sha1(cls, sha1):
+ """Uniquely abbreviates the given SHA1."""
+
+ # For now we invoke git-rev-parse(1), but hopefully eventually
+ # we will be able to do this via pygit2.
+ cmd = ['git', 'rev-parse', '--short', sha1]
+ # cls.logger.debug(" ".join(cmd))
+ out = subprocess.check_output(cmd).strip()
+ # cls.logger.debug(out)
+ return out
+
+ @classmethod
+ def describe(cls, sha1):
+ """Returns a human-readable representation of the given SHA1."""
+
+ # For now we invoke git-describe(1), but eventually we will be
+ # able to do this via pygit2, since libgit2 already provides
+ # an API for this:
+ # https://github.com/libgit2/pygit2/pull/459#issuecomment-68866929
+ # https://github.com/libgit2/libgit2/pull/2592
+ cmd = [
+ 'git', 'describe',
+ '--all', # look for tags and branches
+ '--long', # remotes/github/master-0-g2b6d591
+ # '--contains',
+ # '--abbrev',
+ sha1
+ ]
+ # cls.logger.debug(" ".join(cmd))
+ out = None
+ try:
+ out = subprocess.check_output(cmd, stderr=subprocess.STDOUT)
+ except subprocess.CalledProcessError as e:
+ if e.output.find('No tags can describe') != -1:
+ return ''
+ raise
+
+ out = out.strip()
+ out = re.sub(r'^(heads|tags|remotes)/', '', out)
+ # We already have the abbreviated SHA1 from abbreviate_sha1()
+ out = re.sub(r'-g[0-9a-f]{7,}$', '', out)
+ # cls.logger.debug(out)
+ return out
+
+ @classmethod
+ def refs_to(cls, sha1, repo):
+ """Returns all refs pointing to the given SHA1."""
+ matching = []
+ for refname in repo.listall_references():
+ symref = repo.lookup_reference(refname)
+ dref = symref.resolve()
+ oid = dref.target
+ commit = repo.get(oid)
+ if commit.hex == sha1:
+ matching.append(symref.shorthand)
+
+ return matching
+
+ @classmethod
+ def rev_list(cls, rev_range):
+ cmd = ['git', 'rev-list', rev_range]
+ return subprocess.check_output(cmd).strip().split('\n')
diff --git a/git_deps/handler.py b/git_deps/handler.py
new file mode 100755
index 0000000..2fe71ad
--- /dev/null
+++ b/git_deps/handler.py
@@ -0,0 +1,38 @@
+#!/usr/bin/python
+
+from __future__ import print_function
+
+import os
+import re
+import subprocess
+import sys
+from urlparse import urlparse, urljoin
+
+def abort(msg, exitcode=1):
+ print(msg, file=sys.stderr)
+ sys.exit(exitcode)
+
+def usage():
+ abort("usage: git-handler URL")
+
+def main(args):
+ if len(args) != 1:
+ usage()
+
+ url = args[0]
+
+ if url.scheme != 'gitfile':
+ abort("URL must use gitfile:// scheme")
+
+ repo = os.path.join(url.netloc, url.path)
+ rev = url.fragment
+ os.chdir(repo)
+
+ subprocess.Popen(['gitk', '--all', '--select-commit=%s' % rev])
+
+def run():
+ main(sys.argv[1:])
+
+
+if __name__ == "__main__":
+ run()
diff --git a/git_deps/html/.gitignore b/git_deps/html/.gitignore
new file mode 100644
index 0000000..68b9e27
--- /dev/null
+++ b/git_deps/html/.gitignore
@@ -0,0 +1,2 @@
+node_modules/
+bower_components/
diff --git a/git_deps/html/css/animate.css b/git_deps/html/css/animate.css
new file mode 100644
index 0000000..f784ce8
--- /dev/null
+++ b/git_deps/html/css/animate.css
@@ -0,0 +1,3158 @@
+@charset "UTF-8";
+/*!
+Animate.css - http://daneden.me/animate
+Licensed under the MIT license - http://opensource.org/licenses/MIT
+
+Copyright (c) 2014 Daniel Eden
+*/
+
+.animated {
+ -webkit-animation-duration: 1s;
+ animation-duration: 1s;
+ -webkit-animation-fill-mode: both;
+ animation-fill-mode: both;
+}
+
+.animated.infinite {
+ -webkit-animation-iteration-count: infinite;
+ animation-iteration-count: infinite;
+}
+
+.animated.hinge {
+ -webkit-animation-duration: 2s;
+ animation-duration: 2s;
+}
+
+@-webkit-keyframes bounce {
+ 0%, 20%, 53%, 80%, 100% {
+ -webkit-transition-timing-function: cubic-bezier(0.215, 0.610, 0.355, 1.000);
+ transition-timing-function: cubic-bezier(0.215, 0.610, 0.355, 1.000);
+ -webkit-transform: translate3d(0,0,0);
+ transform: translate3d(0,0,0);
+ }
+
+ 40%, 43% {
+ -webkit-transition-timing-function: cubic-bezier(0.755, 0.050, 0.855, 0.060);
+ transition-timing-function: cubic-bezier(0.755, 0.050, 0.855, 0.060);
+ -webkit-transform: translate3d(0, -30px, 0);
+ transform: translate3d(0, -30px, 0);
+ }
+
+ 70% {
+ -webkit-transition-timing-function: cubic-bezier(0.755, 0.050, 0.855, 0.060);
+ transition-timing-function: cubic-bezier(0.755, 0.050, 0.855, 0.060);
+ -webkit-transform: translate3d(0, -15px, 0);
+ transform: translate3d(0, -15px, 0);
+ }
+
+ 90% {
+ -webkit-transform: translate3d(0,-4px,0);
+ transform: translate3d(0,-4px,0);
+ }
+}
+
+@keyframes bounce {
+ 0%, 20%, 53%, 80%, 100% {
+ -webkit-transition-timing-function: cubic-bezier(0.215, 0.610, 0.355, 1.000);
+ transition-timing-function: cubic-bezier(0.215, 0.610, 0.355, 1.000);
+ -webkit-transform: translate3d(0,0,0);
+ transform: translate3d(0,0,0);
+ }
+
+ 40%, 43% {
+ -webkit-transition-timing-function: cubic-bezier(0.755, 0.050, 0.855, 0.060);
+ transition-timing-function: cubic-bezier(0.755, 0.050, 0.855, 0.060);
+ -webkit-transform: translate3d(0, -30px, 0);
+ transform: translate3d(0, -30px, 0);
+ }
+
+ 70% {
+ -webkit-transition-timing-function: cubic-bezier(0.755, 0.050, 0.855, 0.060);
+ transition-timing-function: cubic-bezier(0.755, 0.050, 0.855, 0.060);
+ -webkit-transform: translate3d(0, -15px, 0);
+ transform: translate3d(0, -15px, 0);
+ }
+
+ 90% {
+ -webkit-transform: translate3d(0,-4px,0);
+ transform: translate3d(0,-4px,0);
+ }
+}
+
+.bounce {
+ -webkit-animation-name: bounce;
+ animation-name: bounce;
+ -webkit-transform-origin: center bottom;
+ -ms-transform-origin: center bottom;
+ transform-origin: center bottom;
+}
+
+@-webkit-keyframes flash {
+ 0%, 50%, 100% {
+ opacity: 1;
+ }
+
+ 25%, 75% {
+ opacity: 0;
+ }
+}
+
+@keyframes flash {
+ 0%, 50%, 100% {
+ opacity: 1;
+ }
+
+ 25%, 75% {
+ opacity: 0;
+ }
+}
+
+.flash {
+ -webkit-animation-name: flash;
+ animation-name: flash;
+}
+
+/* originally authored by Nick Pettit - https://github.com/nickpettit/glide */
+
+@-webkit-keyframes pulse {
+ 0% {
+ -webkit-transform: scale3d(1, 1, 1);
+ transform: scale3d(1, 1, 1);
+ }
+
+ 50% {
+ -webkit-transform: scale3d(1.05, 1.05, 1.05);
+ transform: scale3d(1.05, 1.05, 1.05);
+ }
+
+ 100% {
+ -webkit-transform: scale3d(1, 1, 1);
+ transform: scale3d(1, 1, 1);
+ }
+}
+
+@keyframes pulse {
+ 0% {
+ -webkit-transform: scale3d(1, 1, 1);
+ transform: scale3d(1, 1, 1);
+ }
+
+ 50% {
+ -webkit-transform: scale3d(1.05, 1.05, 1.05);
+ transform: scale3d(1.05, 1.05, 1.05);
+ }
+
+ 100% {
+ -webkit-transform: scale3d(1, 1, 1);
+ transform: scale3d(1, 1, 1);
+ }
+}
+
+.pulse {
+ -webkit-animation-name: pulse;
+ animation-name: pulse;
+}
+
+@-webkit-keyframes rubberBand {
+ 0% {
+ -webkit-transform: scale3d(1, 1, 1);
+ transform: scale3d(1, 1, 1);
+ }
+
+ 30% {
+ -webkit-transform: scale3d(1.25, 0.75, 1);
+ transform: scale3d(1.25, 0.75, 1);
+ }
+
+ 40% {
+ -webkit-transform: scale3d(0.75, 1.25, 1);
+ transform: scale3d(0.75, 1.25, 1);
+ }
+
+ 50% {
+ -webkit-transform: scale3d(1.15, 0.85, 1);
+ transform: scale3d(1.15, 0.85, 1);
+ }
+
+ 65% {
+ -webkit-transform: scale3d(.95, 1.05, 1);
+ transform: scale3d(.95, 1.05, 1);
+ }
+
+ 75% {
+ -webkit-transform: scale3d(1.05, .95, 1);
+ transform: scale3d(1.05, .95, 1);
+ }
+
+ 100% {
+ -webkit-transform: scale3d(1, 1, 1);
+ transform: scale3d(1, 1, 1);
+ }
+}
+
+@keyframes rubberBand {
+ 0% {
+ -webkit-transform: scale3d(1, 1, 1);
+ transform: scale3d(1, 1, 1);
+ }
+
+ 30% {
+ -webkit-transform: scale3d(1.25, 0.75, 1);
+ transform: scale3d(1.25, 0.75, 1);
+ }
+
+ 40% {
+ -webkit-transform: scale3d(0.75, 1.25, 1);
+ transform: scale3d(0.75, 1.25, 1);
+ }
+
+ 50% {
+ -webkit-transform: scale3d(1.15, 0.85, 1);
+ transform: scale3d(1.15, 0.85, 1);
+ }
+
+ 65% {
+ -webkit-transform: scale3d(.95, 1.05, 1);
+ transform: scale3d(.95, 1.05, 1);
+ }
+
+ 75% {
+ -webkit-transform: scale3d(1.05, .95, 1);
+ transform: scale3d(1.05, .95, 1);
+ }
+
+ 100% {
+ -webkit-transform: scale3d(1, 1, 1);
+ transform: scale3d(1, 1, 1);
+ }
+}
+
+.rubberBand {
+ -webkit-animation-name: rubberBand;
+ animation-name: rubberBand;
+}
+
+@-webkit-keyframes shake {
+ 0%, 100% {
+ -webkit-transform: translate3d(0, 0, 0);
+ transform: translate3d(0, 0, 0);
+ }
+
+ 10%, 30%, 50%, 70%, 90% {
+ -webkit-transform: translate3d(-10px, 0, 0);
+ transform: translate3d(-10px, 0, 0);
+ }
+
+ 20%, 40%, 60%, 80% {
+ -webkit-transform: translate3d(10px, 0, 0);
+ transform: translate3d(10px, 0, 0);
+ }
+}
+
+@keyframes shake {
+ 0%, 100% {
+ -webkit-transform: translate3d(0, 0, 0);
+ transform: translate3d(0, 0, 0);
+ }
+
+ 10%, 30%, 50%, 70%, 90% {
+ -webkit-transform: translate3d(-10px, 0, 0);
+ transform: translate3d(-10px, 0, 0);
+ }
+
+ 20%, 40%, 60%, 80% {
+ -webkit-transform: translate3d(10px, 0, 0);
+ transform: translate3d(10px, 0, 0);
+ }
+}
+
+.shake {
+ -webkit-animation-name: shake;
+ animation-name: shake;
+}
+
+@-webkit-keyframes swing {
+ 20% {
+ -webkit-transform: rotate3d(0, 0, 1, 15deg);
+ transform: rotate3d(0, 0, 1, 15deg);
+ }
+
+ 40% {
+ -webkit-transform: rotate3d(0, 0, 1, -10deg);
+ transform: rotate3d(0, 0, 1, -10deg);
+ }
+
+ 60% {
+ -webkit-transform: rotate3d(0, 0, 1, 5deg);
+ transform: rotate3d(0, 0, 1, 5deg);
+ }
+
+ 80% {
+ -webkit-transform: rotate3d(0, 0, 1, -5deg);
+ transform: rotate3d(0, 0, 1, -5deg);
+ }
+
+ 100% {
+ -webkit-transform: rotate3d(0, 0, 1, 0deg);
+ transform: rotate3d(0, 0, 1, 0deg);
+ }
+}
+
+@keyframes swing {
+ 20% {
+ -webkit-transform: rotate3d(0, 0, 1, 15deg);
+ transform: rotate3d(0, 0, 1, 15deg);
+ }
+
+ 40% {
+ -webkit-transform: rotate3d(0, 0, 1, -10deg);
+ transform: rotate3d(0, 0, 1, -10deg);
+ }
+
+ 60% {
+ -webkit-transform: rotate3d(0, 0, 1, 5deg);
+ transform: rotate3d(0, 0, 1, 5deg);
+ }
+
+ 80% {
+ -webkit-transform: rotate3d(0, 0, 1, -5deg);
+ transform: rotate3d(0, 0, 1, -5deg);
+ }
+
+ 100% {
+ -webkit-transform: rotate3d(0, 0, 1, 0deg);
+ transform: rotate3d(0, 0, 1, 0deg);
+ }
+}
+
+.swing {
+ -webkit-transform-origin: top center;
+ -ms-transform-origin: top center;
+ transform-origin: top center;
+ -webkit-animation-name: swing;
+ animation-name: swing;
+}
+
+@-webkit-keyframes tada {
+ 0% {
+ -webkit-transform: scale3d(1, 1, 1);
+ transform: scale3d(1, 1, 1);
+ }
+
+ 10%, 20% {
+ -webkit-transform: scale3d(.9, .9, .9) rotate3d(0, 0, 1, -3deg);
+ transform: scale3d(.9, .9, .9) rotate3d(0, 0, 1, -3deg);
+ }
+
+ 30%, 50%, 70%, 90% {
+ -webkit-transform: scale3d(1.1, 1.1, 1.1) rotate3d(0, 0, 1, 3deg);
+ transform: scale3d(1.1, 1.1, 1.1) rotate3d(0, 0, 1, 3deg);
+ }
+
+ 40%, 60%, 80% {
+ -webkit-transform: scale3d(1.1, 1.1, 1.1) rotate3d(0, 0, 1, -3deg);
+ transform: scale3d(1.1, 1.1, 1.1) rotate3d(0, 0, 1, -3deg);
+ }
+
+ 100% {
+ -webkit-transform: scale3d(1, 1, 1);
+ transform: scale3d(1, 1, 1);
+ }
+}
+
+@keyframes tada {
+ 0% {
+ -webkit-transform: scale3d(1, 1, 1);
+ transform: scale3d(1, 1, 1);
+ }
+
+ 10%, 20% {
+ -webkit-transform: scale3d(.9, .9, .9) rotate3d(0, 0, 1, -3deg);
+ transform: scale3d(.9, .9, .9) rotate3d(0, 0, 1, -3deg);
+ }
+
+ 30%, 50%, 70%, 90% {
+ -webkit-transform: scale3d(1.1, 1.1, 1.1) rotate3d(0, 0, 1, 3deg);
+ transform: scale3d(1.1, 1.1, 1.1) rotate3d(0, 0, 1, 3deg);
+ }
+
+ 40%, 60%, 80% {
+ -webkit-transform: scale3d(1.1, 1.1, 1.1) rotate3d(0, 0, 1, -3deg);
+ transform: scale3d(1.1, 1.1, 1.1) rotate3d(0, 0, 1, -3deg);
+ }
+
+ 100% {
+ -webkit-transform: scale3d(1, 1, 1);
+ transform: scale3d(1, 1, 1);
+ }
+}
+
+.tada {
+ -webkit-animation-name: tada;
+ animation-name: tada;
+}
+
+/* originally authored by Nick Pettit - https://github.com/nickpettit/glide */
+
+@-webkit-keyframes wobble {
+ 0% {
+ -webkit-transform: none;
+ transform: none;
+ }
+
+ 15% {
+ -webkit-transform: translate3d(-25%, 0, 0) rotate3d(0, 0, 1, -5deg);
+ transform: translate3d(-25%, 0, 0) rotate3d(0, 0, 1, -5deg);
+ }
+
+ 30% {
+ -webkit-transform: translate3d(20%, 0, 0) rotate3d(0, 0, 1, 3deg);
+ transform: translate3d(20%, 0, 0) rotate3d(0, 0, 1, 3deg);
+ }
+
+ 45% {
+ -webkit-transform: translate3d(-15%, 0, 0) rotate3d(0, 0, 1, -3deg);
+ transform: translate3d(-15%, 0, 0) rotate3d(0, 0, 1, -3deg);
+ }
+
+ 60% {
+ -webkit-transform: translate3d(10%, 0, 0) rotate3d(0, 0, 1, 2deg);
+ transform: translate3d(10%, 0, 0) rotate3d(0, 0, 1, 2deg);
+ }
+
+ 75% {
+ -webkit-transform: translate3d(-5%, 0, 0) rotate3d(0, 0, 1, -1deg);
+ transform: translate3d(-5%, 0, 0) rotate3d(0, 0, 1, -1deg);
+ }
+
+ 100% {
+ -webkit-transform: none;
+ transform: none;
+ }
+}
+
+@keyframes wobble {
+ 0% {
+ -webkit-transform: none;
+ transform: none;
+ }
+
+ 15% {
+ -webkit-transform: translate3d(-25%, 0, 0) rotate3d(0, 0, 1, -5deg);
+ transform: translate3d(-25%, 0, 0) rotate3d(0, 0, 1, -5deg);
+ }
+
+ 30% {
+ -webkit-transform: translate3d(20%, 0, 0) rotate3d(0, 0, 1, 3deg);
+ transform: translate3d(20%, 0, 0) rotate3d(0, 0, 1, 3deg);
+ }
+
+ 45% {
+ -webkit-transform: translate3d(-15%, 0, 0) rotate3d(0, 0, 1, -3deg);
+ transform: translate3d(-15%, 0, 0) rotate3d(0, 0, 1, -3deg);
+ }
+
+ 60% {
+ -webkit-transform: translate3d(10%, 0, 0) rotate3d(0, 0, 1, 2deg);
+ transform: translate3d(10%, 0, 0) rotate3d(0, 0, 1, 2deg);
+ }
+
+ 75% {
+ -webkit-transform: translate3d(-5%, 0, 0) rotate3d(0, 0, 1, -1deg);
+ transform: translate3d(-5%, 0, 0) rotate3d(0, 0, 1, -1deg);
+ }
+
+ 100% {
+ -webkit-transform: none;
+ transform: none;
+ }
+}
+
+.wobble {
+ -webkit-animation-name: wobble;
+ animation-name: wobble;
+}
+
+@-webkit-keyframes bounceIn {
+ 0%, 20%, 40%, 60%, 80%, 100% {
+ -webkit-transition-timing-function: cubic-bezier(0.215, 0.610, 0.355, 1.000);
+ transition-timing-function: cubic-bezier(0.215, 0.610, 0.355, 1.000);
+ }
+
+ 0% {
+ opacity: 0;
+ -webkit-transform: scale3d(.3, .3, .3);
+ transform: scale3d(.3, .3, .3);
+ }
+
+ 20% {
+ -webkit-transform: scale3d(1.1, 1.1, 1.1);
+ transform: scale3d(1.1, 1.1, 1.1);
+ }
+
+ 40% {
+ -webkit-transform: scale3d(.9, .9, .9);
+ transform: scale3d(.9, .9, .9);
+ }
+
+ 60% {
+ opacity: 1;
+ -webkit-transform: scale3d(1.03, 1.03, 1.03);
+ transform: scale3d(1.03, 1.03, 1.03);
+ }
+
+ 80% {
+ -webkit-transform: scale3d(.97, .97, .97);
+ transform: scale3d(.97, .97, .97);
+ }
+
+ 100% {
+ opacity: 1;
+ -webkit-transform: scale3d(1, 1, 1);
+ transform: scale3d(1, 1, 1);
+ }
+}
+
+@keyframes bounceIn {
+ 0%, 20%, 40%, 60%, 80%, 100% {
+ -webkit-transition-timing-function: cubic-bezier(0.215, 0.610, 0.355, 1.000);
+ transition-timing-function: cubic-bezier(0.215, 0.610, 0.355, 1.000);
+ }
+
+ 0% {
+ opacity: 0;
+ -webkit-transform: scale3d(.3, .3, .3);
+ transform: scale3d(.3, .3, .3);
+ }
+
+ 20% {
+ -webkit-transform: scale3d(1.1, 1.1, 1.1);
+ transform: scale3d(1.1, 1.1, 1.1);
+ }
+
+ 40% {
+ -webkit-transform: scale3d(.9, .9, .9);
+ transform: scale3d(.9, .9, .9);
+ }
+
+ 60% {
+ opacity: 1;
+ -webkit-transform: scale3d(1.03, 1.03, 1.03);
+ transform: scale3d(1.03, 1.03, 1.03);
+ }
+
+ 80% {
+ -webkit-transform: scale3d(.97, .97, .97);
+ transform: scale3d(.97, .97, .97);
+ }
+
+ 100% {
+ opacity: 1;
+ -webkit-transform: scale3d(1, 1, 1);
+ transform: scale3d(1, 1, 1);
+ }
+}
+
+.bounceIn {
+ -webkit-animation-name: bounceIn;
+ animation-name: bounceIn;
+ -webkit-animation-duration: .75s;
+ animation-duration: .75s;
+}
+
+@-webkit-keyframes bounceInDown {
+ 0%, 60%, 75%, 90%, 100% {
+ -webkit-transition-timing-function: cubic-bezier(0.215, 0.610, 0.355, 1.000);
+ transition-timing-function: cubic-bezier(0.215, 0.610, 0.355, 1.000);
+ }
+
+ 0% {
+ opacity: 0;
+ -webkit-transform: translate3d(0, -3000px, 0);
+ transform: translate3d(0, -3000px, 0);
+ }
+
+ 60% {
+ opacity: 1;
+ -webkit-transform: translate3d(0, 25px, 0);
+ transform: translate3d(0, 25px, 0);
+ }
+
+ 75% {
+ -webkit-transform: translate3d(0, -10px, 0);
+ transform: translate3d(0, -10px, 0);
+ }
+
+ 90% {
+ -webkit-transform: translate3d(0, 5px, 0);
+ transform: translate3d(0, 5px, 0);
+ }
+
+ 100% {
+ -webkit-transform: none;
+ transform: none;
+ }
+}
+
+@keyframes bounceInDown {
+ 0%, 60%, 75%, 90%, 100% {
+ -webkit-transition-timing-function: cubic-bezier(0.215, 0.610, 0.355, 1.000);
+ transition-timing-function: cubic-bezier(0.215, 0.610, 0.355, 1.000);
+ }
+
+ 0% {
+ opacity: 0;
+ -webkit-transform: translate3d(0, -3000px, 0);
+ transform: translate3d(0, -3000px, 0);
+ }
+
+ 60% {
+ opacity: 1;
+ -webkit-transform: translate3d(0, 25px, 0);
+ transform: translate3d(0, 25px, 0);
+ }
+
+ 75% {
+ -webkit-transform: translate3d(0, -10px, 0);
+ transform: translate3d(0, -10px, 0);
+ }
+
+ 90% {
+ -webkit-transform: translate3d(0, 5px, 0);
+ transform: translate3d(0, 5px, 0);
+ }
+
+ 100% {
+ -webkit-transform: none;
+ transform: none;
+ }
+}
+
+.bounceInDown {
+ -webkit-animation-name: bounceInDown;
+ animation-name: bounceInDown;
+}
+
+@-webkit-keyframes bounceInLeft {
+ 0%, 60%, 75%, 90%, 100% {
+ -webkit-transition-timing-function: cubic-bezier(0.215, 0.610, 0.355, 1.000);
+ transition-timing-function: cubic-bezier(0.215, 0.610, 0.355, 1.000);
+ }
+
+ 0% {
+ opacity: 0;
+ -webkit-transform: translate3d(-3000px, 0, 0);
+ transform: translate3d(-3000px, 0, 0);
+ }
+
+ 60% {
+ opacity: 1;
+ -webkit-transform: translate3d(25px, 0, 0);
+ transform: translate3d(25px, 0, 0);
+ }
+
+ 75% {
+ -webkit-transform: translate3d(-10px, 0, 0);
+ transform: translate3d(-10px, 0, 0);
+ }
+
+ 90% {
+ -webkit-transform: translate3d(5px, 0, 0);
+ transform: translate3d(5px, 0, 0);
+ }
+
+ 100% {
+ -webkit-transform: none;
+ transform: none;
+ }
+}
+
+@keyframes bounceInLeft {
+ 0%, 60%, 75%, 90%, 100% {
+ -webkit-transition-timing-function: cubic-bezier(0.215, 0.610, 0.355, 1.000);
+ transition-timing-function: cubic-bezier(0.215, 0.610, 0.355, 1.000);
+ }
+
+ 0% {
+ opacity: 0;
+ -webkit-transform: translate3d(-3000px, 0, 0);
+ transform: translate3d(-3000px, 0, 0);
+ }
+
+ 60% {
+ opacity: 1;
+ -webkit-transform: translate3d(25px, 0, 0);
+ transform: translate3d(25px, 0, 0);
+ }
+
+ 75% {
+ -webkit-transform: translate3d(-10px, 0, 0);
+ transform: translate3d(-10px, 0, 0);
+ }
+
+ 90% {
+ -webkit-transform: translate3d(5px, 0, 0);
+ transform: translate3d(5px, 0, 0);
+ }
+
+ 100% {
+ -webkit-transform: none;
+ transform: none;
+ }
+}
+
+.bounceInLeft {
+ -webkit-animation-name: bounceInLeft;
+ animation-name: bounceInLeft;
+}
+
+@-webkit-keyframes bounceInRight {
+ 0%, 60%, 75%, 90%, 100% {
+ -webkit-transition-timing-function: cubic-bezier(0.215, 0.610, 0.355, 1.000);
+ transition-timing-function: cubic-bezier(0.215, 0.610, 0.355, 1.000);
+ }
+
+ 0% {
+ opacity: 0;
+ -webkit-transform: translate3d(3000px, 0, 0);
+ transform: translate3d(3000px, 0, 0);
+ }
+
+ 60% {
+ opacity: 1;
+ -webkit-transform: translate3d(-25px, 0, 0);
+ transform: translate3d(-25px, 0, 0);
+ }
+
+ 75% {
+ -webkit-transform: translate3d(10px, 0, 0);
+ transform: translate3d(10px, 0, 0);
+ }
+
+ 90% {
+ -webkit-transform: translate3d(-5px, 0, 0);
+ transform: translate3d(-5px, 0, 0);
+ }
+
+ 100% {
+ -webkit-transform: none;
+ transform: none;
+ }
+}
+
+@keyframes bounceInRight {
+ 0%, 60%, 75%, 90%, 100% {
+ -webkit-transition-timing-function: cubic-bezier(0.215, 0.610, 0.355, 1.000);
+ transition-timing-function: cubic-bezier(0.215, 0.610, 0.355, 1.000);
+ }
+
+ 0% {
+ opacity: 0;
+ -webkit-transform: translate3d(3000px, 0, 0);
+ transform: translate3d(3000px, 0, 0);
+ }
+
+ 60% {
+ opacity: 1;
+ -webkit-transform: translate3d(-25px, 0, 0);
+ transform: translate3d(-25px, 0, 0);
+ }
+
+ 75% {
+ -webkit-transform: translate3d(10px, 0, 0);
+ transform: translate3d(10px, 0, 0);
+ }
+
+ 90% {
+ -webkit-transform: translate3d(-5px, 0, 0);
+ transform: translate3d(-5px, 0, 0);
+ }
+
+ 100% {
+ -webkit-transform: none;
+ transform: none;
+ }
+}
+
+.bounceInRight {
+ -webkit-animation-name: bounceInRight;
+ animation-name: bounceInRight;
+}
+
+@-webkit-keyframes bounceInUp {
+ 0%, 60%, 75%, 90%, 100% {
+ -webkit-transition-timing-function: cubic-bezier(0.215, 0.610, 0.355, 1.000);
+ transition-timing-function: cubic-bezier(0.215, 0.610, 0.355, 1.000);
+ }
+
+ 0% {
+ opacity: 0;
+ -webkit-transform: translate3d(0, 3000px, 0);
+ transform: translate3d(0, 3000px, 0);
+ }
+
+ 60% {
+ opacity: 1;
+ -webkit-transform: translate3d(0, -20px, 0);
+ transform: translate3d(0, -20px, 0);
+ }
+
+ 75% {
+ -webkit-transform: translate3d(0, 10px, 0);
+ transform: translate3d(0, 10px, 0);
+ }
+
+ 90% {
+ -webkit-transform: translate3d(0, -5px, 0);
+ transform: translate3d(0, -5px, 0);
+ }
+
+ 100% {
+ -webkit-transform: translate3d(0, 0, 0);
+ transform: translate3d(0, 0, 0);
+ }
+}
+
+@keyframes bounceInUp {
+ 0%, 60%, 75%, 90%, 100% {
+ -webkit-transition-timing-function: cubic-bezier(0.215, 0.610, 0.355, 1.000);
+ transition-timing-function: cubic-bezier(0.215, 0.610, 0.355, 1.000);
+ }
+
+ 0% {
+ opacity: 0;
+ -webkit-transform: translate3d(0, 3000px, 0);
+ transform: translate3d(0, 3000px, 0);
+ }
+
+ 60% {
+ opacity: 1;
+ -webkit-transform: translate3d(0, -20px, 0);
+ transform: translate3d(0, -20px, 0);
+ }
+
+ 75% {
+ -webkit-transform: translate3d(0, 10px, 0);
+ transform: translate3d(0, 10px, 0);
+ }
+
+ 90% {
+ -webkit-transform: translate3d(0, -5px, 0);
+ transform: translate3d(0, -5px, 0);
+ }
+
+ 100% {
+ -webkit-transform: translate3d(0, 0, 0);
+ transform: translate3d(0, 0, 0);
+ }
+}
+
+.bounceInUp {
+ -webkit-animation-name: bounceInUp;
+ animation-name: bounceInUp;
+}
+
+@-webkit-keyframes bounceOut {
+ 20% {
+ -webkit-transform: scale3d(.9, .9, .9);
+ transform: scale3d(.9, .9, .9);
+ }
+
+ 50%, 55% {
+ opacity: 1;
+ -webkit-transform: scale3d(1.1, 1.1, 1.1);
+ transform: scale3d(1.1, 1.1, 1.1);
+ }
+
+ 100% {
+ opacity: 0;
+ -webkit-transform: scale3d(.3, .3, .3);
+ transform: scale3d(.3, .3, .3);
+ }
+}
+
+@keyframes bounceOut {
+ 20% {
+ -webkit-transform: scale3d(.9, .9, .9);
+ transform: scale3d(.9, .9, .9);
+ }
+
+ 50%, 55% {
+ opacity: 1;
+ -webkit-transform: scale3d(1.1, 1.1, 1.1);
+ transform: scale3d(1.1, 1.1, 1.1);
+ }
+
+ 100% {
+ opacity: 0;
+ -webkit-transform: scale3d(.3, .3, .3);
+ transform: scale3d(.3, .3, .3);
+ }
+}
+
+.bounceOut {
+ -webkit-animation-name: bounceOut;
+ animation-name: bounceOut;
+ -webkit-animation-duration: .75s;
+ animation-duration: .75s;
+}
+
+@-webkit-keyframes bounceOutDown {
+ 20% {
+ -webkit-transform: translate3d(0, 10px, 0);
+ transform: translate3d(0, 10px, 0);
+ }
+
+ 40%, 45% {
+ opacity: 1;
+ -webkit-transform: translate3d(0, -20px, 0);
+ transform: translate3d(0, -20px, 0);
+ }
+
+ 100% {
+ opacity: 0;
+ -webkit-transform: translate3d(0, 2000px, 0);
+ transform: translate3d(0, 2000px, 0);
+ }
+}
+
+@keyframes bounceOutDown {
+ 20% {
+ -webkit-transform: translate3d(0, 10px, 0);
+ transform: translate3d(0, 10px, 0);
+ }
+
+ 40%, 45% {
+ opacity: 1;
+ -webkit-transform: translate3d(0, -20px, 0);
+ transform: translate3d(0, -20px, 0);
+ }
+
+ 100% {
+ opacity: 0;
+ -webkit-transform: translate3d(0, 2000px, 0);
+ transform: translate3d(0, 2000px, 0);
+ }
+}
+
+.bounceOutDown {
+ -webkit-animation-name: bounceOutDown;
+ animation-name: bounceOutDown;
+}
+
+@-webkit-keyframes bounceOutLeft {
+ 20% {
+ opacity: 1;
+ -webkit-transform: translate3d(20px, 0, 0);
+ transform: translate3d(20px, 0, 0);
+ }
+
+ 100% {
+ opacity: 0;
+ -webkit-transform: translate3d(-2000px, 0, 0);
+ transform: translate3d(-2000px, 0, 0);
+ }
+}
+
+@keyframes bounceOutLeft {
+ 20% {
+ opacity: 1;
+ -webkit-transform: translate3d(20px, 0, 0);
+ transform: translate3d(20px, 0, 0);
+ }
+
+ 100% {
+ opacity: 0;
+ -webkit-transform: translate3d(-2000px, 0, 0);
+ transform: translate3d(-2000px, 0, 0);
+ }
+}
+
+.bounceOutLeft {
+ -webkit-animation-name: bounceOutLeft;
+ animation-name: bounceOutLeft;
+}
+
+@-webkit-keyframes bounceOutRight {
+ 20% {
+ opacity: 1;
+ -webkit-transform: translate3d(-20px, 0, 0);
+ transform: translate3d(-20px, 0, 0);
+ }
+
+ 100% {
+ opacity: 0;
+ -webkit-transform: translate3d(2000px, 0, 0);
+ transform: translate3d(2000px, 0, 0);
+ }
+}
+
+@keyframes bounceOutRight {
+ 20% {
+ opacity: 1;
+ -webkit-transform: translate3d(-20px, 0, 0);
+ transform: translate3d(-20px, 0, 0);
+ }
+
+ 100% {
+ opacity: 0;
+ -webkit-transform: translate3d(2000px, 0, 0);
+ transform: translate3d(2000px, 0, 0);
+ }
+}
+
+.bounceOutRight {
+ -webkit-animation-name: bounceOutRight;
+ animation-name: bounceOutRight;
+}
+
+@-webkit-keyframes bounceOutUp {
+ 20% {
+ -webkit-transform: translate3d(0, -10px, 0);
+ transform: translate3d(0, -10px, 0);
+ }
+
+ 40%, 45% {
+ opacity: 1;
+ -webkit-transform: translate3d(0, 20px, 0);
+ transform: translate3d(0, 20px, 0);
+ }
+
+ 100% {
+ opacity: 0;
+ -webkit-transform: translate3d(0, -2000px, 0);
+ transform: translate3d(0, -2000px, 0);
+ }
+}
+
+@keyframes bounceOutUp {
+ 20% {
+ -webkit-transform: translate3d(0, -10px, 0);
+ transform: translate3d(0, -10px, 0);
+ }
+
+ 40%, 45% {
+ opacity: 1;
+ -webkit-transform: translate3d(0, 20px, 0);
+ transform: translate3d(0, 20px, 0);
+ }
+
+ 100% {
+ opacity: 0;
+ -webkit-transform: translate3d(0, -2000px, 0);
+ transform: translate3d(0, -2000px, 0);
+ }
+}
+
+.bounceOutUp {
+ -webkit-animation-name: bounceOutUp;
+ animation-name: bounceOutUp;
+}
+
+@-webkit-keyframes fadeIn {
+ 0% {opacity: 0;}
+ 100% {opacity: 1;}
+}
+
+@keyframes fadeIn {
+ 0% {opacity: 0;}
+ 100% {opacity: 1;}
+}
+
+.fadeIn {
+ -webkit-animation-name: fadeIn;
+ animation-name: fadeIn;
+}
+
+@-webkit-keyframes fadeInDown {
+ 0% {
+ opacity: 0;
+ -webkit-transform: translate3d(0, -100%, 0);
+ transform: translate3d(0, -100%, 0);
+ }
+
+ 100% {
+ opacity: 1;
+ -webkit-transform: none;
+ transform: none;
+ }
+}
+
+@keyframes fadeInDown {
+ 0% {
+ opacity: 0;
+ -webkit-transform: translate3d(0, -100%, 0);
+ transform: translate3d(0, -100%, 0);
+ }
+
+ 100% {
+ opacity: 1;
+ -webkit-transform: none;
+ transform: none;
+ }
+}
+
+.fadeInDown {
+ -webkit-animation-name: fadeInDown;
+ animation-name: fadeInDown;
+}
+
+@-webkit-keyframes fadeInDownBig {
+ 0% {
+ opacity: 0;
+ -webkit-transform: translate3d(0, -2000px, 0);
+ transform: translate3d(0, -2000px, 0);
+ }
+
+ 100% {
+ opacity: 1;
+ -webkit-transform: none;
+ transform: none;
+ }
+}
+
+@keyframes fadeInDownBig {
+ 0% {
+ opacity: 0;
+ -webkit-transform: translate3d(0, -2000px, 0);
+ transform: translate3d(0, -2000px, 0);
+ }
+
+ 100% {
+ opacity: 1;
+ -webkit-transform: none;
+ transform: none;
+ }
+}
+
+.fadeInDownBig {
+ -webkit-animation-name: fadeInDownBig;
+ animation-name: fadeInDownBig;
+}
+
+@-webkit-keyframes fadeInLeft {
+ 0% {
+ opacity: 0;
+ -webkit-transform: translate3d(-100%, 0, 0);
+ transform: translate3d(-100%, 0, 0);
+ }
+
+ 100% {
+ opacity: 1;
+ -webkit-transform: none;
+ transform: none;
+ }
+}
+
+@keyframes fadeInLeft {
+ 0% {
+ opacity: 0;
+ -webkit-transform: translate3d(-100%, 0, 0);
+ transform: translate3d(-100%, 0, 0);
+ }
+
+ 100% {
+ opacity: 1;
+ -webkit-transform: none;
+ transform: none;
+ }
+}
+
+.fadeInLeft {
+ -webkit-animation-name: fadeInLeft;
+ animation-name: fadeInLeft;
+}
+
+@-webkit-keyframes fadeInLeftBig {
+ 0% {
+ opacity: 0;
+ -webkit-transform: translate3d(-2000px, 0, 0);
+ transform: translate3d(-2000px, 0, 0);
+ }
+
+ 100% {
+ opacity: 1;
+ -webkit-transform: none;
+ transform: none;
+ }
+}
+
+@keyframes fadeInLeftBig {
+ 0% {
+ opacity: 0;
+ -webkit-transform: translate3d(-2000px, 0, 0);
+ transform: translate3d(-2000px, 0, 0);
+ }
+
+ 100% {
+ opacity: 1;
+ -webkit-transform: none;
+ transform: none;
+ }
+}
+
+.fadeInLeftBig {
+ -webkit-animation-name: fadeInLeftBig;
+ animation-name: fadeInLeftBig;
+}
+
+@-webkit-keyframes fadeInRight {
+ 0% {
+ opacity: 0;
+ -webkit-transform: translate3d(100%, 0, 0);
+ transform: translate3d(100%, 0, 0);
+ }
+
+ 100% {
+ opacity: 1;
+ -webkit-transform: none;
+ transform: none;
+ }
+}
+
+@keyframes fadeInRight {
+ 0% {
+ opacity: 0;
+ -webkit-transform: translate3d(100%, 0, 0);
+ transform: translate3d(100%, 0, 0);
+ }
+
+ 100% {
+ opacity: 1;
+ -webkit-transform: none;
+ transform: none;
+ }
+}
+
+.fadeInRight {
+ -webkit-animation-name: fadeInRight;
+ animation-name: fadeInRight;
+}
+
+@-webkit-keyframes fadeInRightBig {
+ 0% {
+ opacity: 0;
+ -webkit-transform: translate3d(2000px, 0, 0);
+ transform: translate3d(2000px, 0, 0);
+ }
+
+ 100% {
+ opacity: 1;
+ -webkit-transform: none;
+ transform: none;
+ }
+}
+
+@keyframes fadeInRightBig {
+ 0% {
+ opacity: 0;
+ -webkit-transform: translate3d(2000px, 0, 0);
+ transform: translate3d(2000px, 0, 0);
+ }
+
+ 100% {
+ opacity: 1;
+ -webkit-transform: none;
+ transform: none;
+ }
+}
+
+.fadeInRightBig {
+ -webkit-animation-name: fadeInRightBig;
+ animation-name: fadeInRightBig;
+}
+
+@-webkit-keyframes fadeInUp {
+ 0% {
+ opacity: 0;
+ -webkit-transform: translate3d(0, 100%, 0);
+ transform: translate3d(0, 100%, 0);
+ }
+
+ 100% {
+ opacity: 1;
+ -webkit-transform: none;
+ transform: none;
+ }
+}
+
+@keyframes fadeInUp {
+ 0% {
+ opacity: 0;
+ -webkit-transform: translate3d(0, 100%, 0);
+ transform: translate3d(0, 100%, 0);
+ }
+
+ 100% {
+ opacity: 1;
+ -webkit-transform: none;
+ transform: none;
+ }
+}
+
+.fadeInUp {
+ -webkit-animation-name: fadeInUp;
+ animation-name: fadeInUp;
+}
+
+@-webkit-keyframes fadeInUpBig {
+ 0% {
+ opacity: 0;
+ -webkit-transform: translate3d(0, 2000px, 0);
+ transform: translate3d(0, 2000px, 0);
+ }
+
+ 100% {
+ opacity: 1;
+ -webkit-transform: none;
+ transform: none;
+ }
+}
+
+@keyframes fadeInUpBig {
+ 0% {
+ opacity: 0;
+ -webkit-transform: translate3d(0, 2000px, 0);
+ transform: translate3d(0, 2000px, 0);
+ }
+
+ 100% {
+ opacity: 1;
+ -webkit-transform: none;
+ transform: none;
+ }
+}
+
+.fadeInUpBig {
+ -webkit-animation-name: fadeInUpBig;
+ animation-name: fadeInUpBig;
+}
+
+@-webkit-keyframes fadeOut {
+ 0% {opacity: 1;}
+ 100% {opacity: 0;}
+}
+
+@keyframes fadeOut {
+ 0% {opacity: 1;}
+ 100% {opacity: 0;}
+}
+
+.fadeOut {
+ -webkit-animation-name: fadeOut;
+ animation-name: fadeOut;
+}
+
+@-webkit-keyframes fadeOutDown {
+ 0% {
+ opacity: 1;
+ }
+
+ 100% {
+ opacity: 0;
+ -webkit-transform: translate3d(0, 100%, 0);
+ transform: translate3d(0, 100%, 0);
+ }
+}
+
+@keyframes fadeOutDown {
+ 0% {
+ opacity: 1;
+ }
+
+ 100% {
+ opacity: 0;
+ -webkit-transform: translate3d(0, 100%, 0);
+ transform: translate3d(0, 100%, 0);
+ }
+}
+
+.fadeOutDown {
+ -webkit-animation-name: fadeOutDown;
+ animation-name: fadeOutDown;
+}
+
+@-webkit-keyframes fadeOutDownBig {
+ 0% {
+ opacity: 1;
+ }
+
+ 100% {
+ opacity: 0;
+ -webkit-transform: translate3d(0, 2000px, 0);
+ transform: translate3d(0, 2000px, 0);
+ }
+}
+
+@keyframes fadeOutDownBig {
+ 0% {
+ opacity: 1;
+ }
+
+ 100% {
+ opacity: 0;
+ -webkit-transform: translate3d(0, 2000px, 0);
+ transform: translate3d(0, 2000px, 0);
+ }
+}
+
+.fadeOutDownBig {
+ -webkit-animation-name: fadeOutDownBig;
+ animation-name: fadeOutDownBig;
+}
+
+@-webkit-keyframes fadeOutLeft {
+ 0% {
+ opacity: 1;
+ }
+
+ 100% {
+ opacity: 0;
+ -webkit-transform: translate3d(-100%, 0, 0);
+ transform: translate3d(-100%, 0, 0);
+ }
+}
+
+@keyframes fadeOutLeft {
+ 0% {
+ opacity: 1;
+ }
+
+ 100% {
+ opacity: 0;
+ -webkit-transform: translate3d(-100%, 0, 0);
+ transform: translate3d(-100%, 0, 0);
+ }
+}
+
+.fadeOutLeft {
+ -webkit-animation-name: fadeOutLeft;
+ animation-name: fadeOutLeft;
+}
+
+@-webkit-keyframes fadeOutLeftBig {
+ 0% {
+ opacity: 1;
+ }
+
+ 100% {
+ opacity: 0;
+ -webkit-transform: translate3d(-2000px, 0, 0);
+ transform: translate3d(-2000px, 0, 0);
+ }
+}
+
+@keyframes fadeOutLeftBig {
+ 0% {
+ opacity: 1;
+ }
+
+ 100% {
+ opacity: 0;
+ -webkit-transform: translate3d(-2000px, 0, 0);
+ transform: translate3d(-2000px, 0, 0);
+ }
+}
+
+.fadeOutLeftBig {
+ -webkit-animation-name: fadeOutLeftBig;
+ animation-name: fadeOutLeftBig;
+}
+
+@-webkit-keyframes fadeOutRight {
+ 0% {
+ opacity: 1;
+ }
+
+ 100% {
+ opacity: 0;
+ -webkit-transform: translate3d(100%, 0, 0);
+ transform: translate3d(100%, 0, 0);
+ }
+}
+
+@keyframes fadeOutRight {
+ 0% {
+ opacity: 1;
+ }
+
+ 100% {
+ opacity: 0;
+ -webkit-transform: translate3d(100%, 0, 0);
+ transform: translate3d(100%, 0, 0);
+ }
+}
+
+.fadeOutRight {
+ -webkit-animation-name: fadeOutRight;
+ animation-name: fadeOutRight;
+}
+
+@-webkit-keyframes fadeOutRightBig {
+ 0% {
+ opacity: 1;
+ }
+
+ 100% {
+ opacity: 0;
+ -webkit-transform: translate3d(2000px, 0, 0);
+ transform: translate3d(2000px, 0, 0);
+ }
+}
+
+@keyframes fadeOutRightBig {
+ 0% {
+ opacity: 1;
+ }
+
+ 100% {
+ opacity: 0;
+ -webkit-transform: translate3d(2000px, 0, 0);
+ transform: translate3d(2000px, 0, 0);
+ }
+}
+
+.fadeOutRightBig {
+ -webkit-animation-name: fadeOutRightBig;
+ animation-name: fadeOutRightBig;
+}
+
+@-webkit-keyframes fadeOutUp {
+ 0% {
+ opacity: 1;
+ }
+
+ 100% {
+ opacity: 0;
+ -webkit-transform: translate3d(0, -100%, 0);
+ transform: translate3d(0, -100%, 0);
+ }
+}
+
+@keyframes fadeOutUp {
+ 0% {
+ opacity: 1;
+ }
+
+ 100% {
+ opacity: 0;
+ -webkit-transform: translate3d(0, -100%, 0);
+ transform: translate3d(0, -100%, 0);
+ }
+}
+
+.fadeOutUp {
+ -webkit-animation-name: fadeOutUp;
+ animation-name: fadeOutUp;
+}
+
+@-webkit-keyframes fadeOutUpBig {
+ 0% {
+ opacity: 1;
+ }
+
+ 100% {
+ opacity: 0;
+ -webkit-transform: translate3d(0, -2000px, 0);
+ transform: translate3d(0, -2000px, 0);
+ }
+}
+
+@keyframes fadeOutUpBig {
+ 0% {
+ opacity: 1;
+ }
+
+ 100% {
+ opacity: 0;
+ -webkit-transform: translate3d(0, -2000px, 0);
+ transform: translate3d(0, -2000px, 0);
+ }
+}
+
+.fadeOutUpBig {
+ -webkit-animation-name: fadeOutUpBig;
+ animation-name: fadeOutUpBig;
+}
+
+@-webkit-keyframes flip {
+ 0% {
+ -webkit-transform: perspective(400px) rotate3d(0, 1, 0, -360deg);
+ transform: perspective(400px) rotate3d(0, 1, 0, -360deg);
+ -webkit-animation-timing-function: ease-out;
+ animation-timing-function: ease-out;
+ }
+
+ 40% {
+ -webkit-transform: perspective(400px) translate3d(0, 0, 150px) rotate3d(0, 1, 0, -190deg);
+ transform: perspective(400px) translate3d(0, 0, 150px) rotate3d(0, 1, 0, -190deg);
+ -webkit-animation-timing-function: ease-out;
+ animation-timing-function: ease-out;
+ }
+
+ 50% {
+ -webkit-transform: perspective(400px) translate3d(0, 0, 150px) rotate3d(0, 1, 0, -170deg);
+ transform: perspective(400px) translate3d(0, 0, 150px) rotate3d(0, 1, 0, -170deg);
+ -webkit-animation-timing-function: ease-in;
+ animation-timing-function: ease-in;
+ }
+
+ 80% {
+ -webkit-transform: perspective(400px) scale3d(.95, .95, .95);
+ transform: perspective(400px) scale3d(.95, .95, .95);
+ -webkit-animation-timing-function: ease-in;
+ animation-timing-function: ease-in;
+ }
+
+ 100% {
+ -webkit-transform: perspective(400px);
+ transform: perspective(400px);
+ -webkit-animation-timing-function: ease-in;
+ animation-timing-function: ease-in;
+ }
+}
+
+@keyframes flip {
+ 0% {
+ -webkit-transform: perspective(400px) rotate3d(0, 1, 0, -360deg);
+ transform: perspective(400px) rotate3d(0, 1, 0, -360deg);
+ -webkit-animation-timing-function: ease-out;
+ animation-timing-function: ease-out;
+ }
+
+ 40% {
+ -webkit-transform: perspective(400px) translate3d(0, 0, 150px) rotate3d(0, 1, 0, -190deg);
+ transform: perspective(400px) translate3d(0, 0, 150px) rotate3d(0, 1, 0, -190deg);
+ -webkit-animation-timing-function: ease-out;
+ animation-timing-function: ease-out;
+ }
+
+ 50% {
+ -webkit-transform: perspective(400px) translate3d(0, 0, 150px) rotate3d(0, 1, 0, -170deg);
+ transform: perspective(400px) translate3d(0, 0, 150px) rotate3d(0, 1, 0, -170deg);
+ -webkit-animation-timing-function: ease-in;
+ animation-timing-function: ease-in;
+ }
+
+ 80% {
+ -webkit-transform: perspective(400px) scale3d(.95, .95, .95);
+ transform: perspective(400px) scale3d(.95, .95, .95);
+ -webkit-animation-timing-function: ease-in;
+ animation-timing-function: ease-in;
+ }
+
+ 100% {
+ -webkit-transform: perspective(400px);
+ transform: perspective(400px);
+ -webkit-animation-timing-function: ease-in;
+ animation-timing-function: ease-in;
+ }
+}
+
+.animated.flip {
+ -webkit-backface-visibility: visible;
+ backface-visibility: visible;
+ -webkit-animation-name: flip;
+ animation-name: flip;
+}
+
+@-webkit-keyframes flipInX {
+ 0% {
+ -webkit-transform: perspective(400px) rotate3d(1, 0, 0, 90deg);
+ transform: perspective(400px) rotate3d(1, 0, 0, 90deg);
+ -webkit-transition-timing-function: ease-in;
+ transition-timing-function: ease-in;
+ opacity: 0;
+ }
+
+ 40% {
+ -webkit-transform: perspective(400px) rotate3d(1, 0, 0, -20deg);
+ transform: perspective(400px) rotate3d(1, 0, 0, -20deg);
+ -webkit-transition-timing-function: ease-in;
+ transition-timing-function: ease-in;
+ }
+
+ 60% {
+ -webkit-transform: perspective(400px) rotate3d(1, 0, 0, 10deg);
+ transform: perspective(400px) rotate3d(1, 0, 0, 10deg);
+ opacity: 1;
+ }
+
+ 80% {
+ -webkit-transform: perspective(400px) rotate3d(1, 0, 0, -5deg);
+ transform: perspective(400px) rotate3d(1, 0, 0, -5deg);
+ }
+
+ 100% {
+ -webkit-transform: perspective(400px);
+ transform: perspective(400px);
+ }
+}
+
+@keyframes flipInX {
+ 0% {
+ -webkit-transform: perspective(400px) rotate3d(1, 0, 0, 90deg);
+ transform: perspective(400px) rotate3d(1, 0, 0, 90deg);
+ -webkit-transition-timing-function: ease-in;
+ transition-timing-function: ease-in;
+ opacity: 0;
+ }
+
+ 40% {
+ -webkit-transform: perspective(400px) rotate3d(1, 0, 0, -20deg);
+ transform: perspective(400px) rotate3d(1, 0, 0, -20deg);
+ -webkit-transition-timing-function: ease-in;
+ transition-timing-function: ease-in;
+ }
+
+ 60% {
+ -webkit-transform: perspective(400px) rotate3d(1, 0, 0, 10deg);
+ transform: perspective(400px) rotate3d(1, 0, 0, 10deg);
+ opacity: 1;
+ }
+
+ 80% {
+ -webkit-transform: perspective(400px) rotate3d(1, 0, 0, -5deg);
+ transform: perspective(400px) rotate3d(1, 0, 0, -5deg);
+ }
+
+ 100% {
+ -webkit-transform: perspective(400px);
+ transform: perspective(400px);
+ }
+}
+
+.flipInX {
+ -webkit-backface-visibility: visible !important;
+ backface-visibility: visible !important;
+ -webkit-animation-name: flipInX;
+ animation-name: flipInX;
+}
+
+@-webkit-keyframes flipInY {
+ 0% {
+ -webkit-transform: perspective(400px) rotate3d(0, 1, 0, 90deg);
+ transform: perspective(400px) rotate3d(0, 1, 0, 90deg);
+ -webkit-transition-timing-function: ease-in;
+ transition-timing-function: ease-in;
+ opacity: 0;
+ }
+
+ 40% {
+ -webkit-transform: perspective(400px) rotate3d(0, 1, 0, -20deg);
+ transform: perspective(400px) rotate3d(0, 1, 0, -20deg);
+ -webkit-transition-timing-function: ease-in;
+ transition-timing-function: ease-in;
+ }
+
+ 60% {
+ -webkit-transform: perspective(400px) rotate3d(0, 1, 0, 10deg);
+ transform: perspective(400px) rotate3d(0, 1, 0, 10deg);
+ opacity: 1;
+ }
+
+ 80% {
+ -webkit-transform: perspective(400px) rotate3d(0, 1, 0, -5deg);
+ transform: perspective(400px) rotate3d(0, 1, 0, -5deg);
+ }
+
+ 100% {
+ -webkit-transform: perspective(400px);
+ transform: perspective(400px);
+ }
+}
+
+@keyframes flipInY {
+ 0% {
+ -webkit-transform: perspective(400px) rotate3d(0, 1, 0, 90deg);
+ transform: perspective(400px) rotate3d(0, 1, 0, 90deg);
+ -webkit-transition-timing-function: ease-in;
+ transition-timing-function: ease-in;
+ opacity: 0;
+ }
+
+ 40% {
+ -webkit-transform: perspective(400px) rotate3d(0, 1, 0, -20deg);
+ transform: perspective(400px) rotate3d(0, 1, 0, -20deg);
+ -webkit-transition-timing-function: ease-in;
+ transition-timing-function: ease-in;
+ }
+
+ 60% {
+ -webkit-transform: perspective(400px) rotate3d(0, 1, 0, 10deg);
+ transform: perspective(400px) rotate3d(0, 1, 0, 10deg);
+ opacity: 1;
+ }
+
+ 80% {
+ -webkit-transform: perspective(400px) rotate3d(0, 1, 0, -5deg);
+ transform: perspective(400px) rotate3d(0, 1, 0, -5deg);
+ }
+
+ 100% {
+ -webkit-transform: perspective(400px);
+ transform: perspective(400px);
+ }
+}
+
+.flipInY {
+ -webkit-backface-visibility: visible !important;
+ backface-visibility: visible !important;
+ -webkit-animation-name: flipInY;
+ animation-name: flipInY;
+}
+
+@-webkit-keyframes flipOutX {
+ 0% {
+ -webkit-transform: perspective(400px);
+ transform: perspective(400px);
+ }
+
+ 30% {
+ -webkit-transform: perspective(400px) rotate3d(1, 0, 0, -20deg);
+ transform: perspective(400px) rotate3d(1, 0, 0, -20deg);
+ opacity: 1;
+ }
+
+ 100% {
+ -webkit-transform: perspective(400px) rotate3d(1, 0, 0, 90deg);
+ transform: perspective(400px) rotate3d(1, 0, 0, 90deg);
+ opacity: 0;
+ }
+}
+
+@keyframes flipOutX {
+ 0% {
+ -webkit-transform: perspective(400px);
+ transform: perspective(400px);
+ }
+
+ 30% {
+ -webkit-transform: perspective(400px) rotate3d(1, 0, 0, -20deg);
+ transform: perspective(400px) rotate3d(1, 0, 0, -20deg);
+ opacity: 1;
+ }
+
+ 100% {
+ -webkit-transform: perspective(400px) rotate3d(1, 0, 0, 90deg);
+ transform: perspective(400px) rotate3d(1, 0, 0, 90deg);
+ opacity: 0;
+ }
+}
+
+.flipOutX {
+ -webkit-animation-name: flipOutX;
+ animation-name: flipOutX;
+ -webkit-animation-duration: .75s;
+ animation-duration: .75s;
+ -webkit-backface-visibility: visible !important;
+ backface-visibility: visible !important;
+}
+
+@-webkit-keyframes flipOutY {
+ 0% {
+ -webkit-transform: perspective(400px);
+ transform: perspective(400px);
+ }
+
+ 30% {
+ -webkit-transform: perspective(400px) rotate3d(0, 1, 0, -15deg);
+ transform: perspective(400px) rotate3d(0, 1, 0, -15deg);
+ opacity: 1;
+ }
+
+ 100% {
+ -webkit-transform: perspective(400px) rotate3d(0, 1, 0, 90deg);
+ transform: perspective(400px) rotate3d(0, 1, 0, 90deg);
+ opacity: 0;
+ }
+}
+
+@keyframes flipOutY {
+ 0% {
+ -webkit-transform: perspective(400px);
+ transform: perspective(400px);
+ }
+
+ 30% {
+ -webkit-transform: perspective(400px) rotate3d(0, 1, 0, -15deg);
+ transform: perspective(400px) rotate3d(0, 1, 0, -15deg);
+ opacity: 1;
+ }
+
+ 100% {
+ -webkit-transform: perspective(400px) rotate3d(0, 1, 0, 90deg);
+ transform: perspective(400px) rotate3d(0, 1, 0, 90deg);
+ opacity: 0;
+ }
+}
+
+.flipOutY {
+ -webkit-backface-visibility: visible !important;
+ backface-visibility: visible !important;
+ -webkit-animation-name: flipOutY;
+ animation-name: flipOutY;
+ -webkit-animation-duration: .75s;
+ animation-duration: .75s;
+}
+
+@-webkit-keyframes lightSpeedIn {
+ 0% {
+ -webkit-transform: translate3d(100%, 0, 0) skewX(-30deg);
+ transform: translate3d(100%, 0, 0) skewX(-30deg);
+ opacity: 0;
+ }
+
+ 60% {
+ -webkit-transform: skewX(20deg);
+ transform: skewX(20deg);
+ opacity: 1;
+ }
+
+ 80% {
+ -webkit-transform: skewX(-5deg);
+ transform: skewX(-5deg);
+ opacity: 1;
+ }
+
+ 100% {
+ -webkit-transform: none;
+ transform: none;
+ opacity: 1;
+ }
+}
+
+@keyframes lightSpeedIn {
+ 0% {
+ -webkit-transform: translate3d(100%, 0, 0) skewX(-30deg);
+ transform: translate3d(100%, 0, 0) skewX(-30deg);
+ opacity: 0;
+ }
+
+ 60% {
+ -webkit-transform: skewX(20deg);
+ transform: skewX(20deg);
+ opacity: 1;
+ }
+
+ 80% {
+ -webkit-transform: skewX(-5deg);
+ transform: skewX(-5deg);
+ opacity: 1;
+ }
+
+ 100% {
+ -webkit-transform: none;
+ transform: none;
+ opacity: 1;
+ }
+}
+
+.lightSpeedIn {
+ -webkit-animation-name: lightSpeedIn;
+ animation-name: lightSpeedIn;
+ -webkit-animation-timing-function: ease-out;
+ animation-timing-function: ease-out;
+}
+
+@-webkit-keyframes lightSpeedOut {
+ 0% {
+ opacity: 1;
+ }
+
+ 100% {
+ -webkit-transform: translate3d(100%, 0, 0) skewX(30deg);
+ transform: translate3d(100%, 0, 0) skewX(30deg);
+ opacity: 0;
+ }
+}
+
+@keyframes lightSpeedOut {
+ 0% {
+ opacity: 1;
+ }
+
+ 100% {
+ -webkit-transform: translate3d(100%, 0, 0) skewX(30deg);
+ transform: translate3d(100%, 0, 0) skewX(30deg);
+ opacity: 0;
+ }
+}
+
+.lightSpeedOut {
+ -webkit-animation-name: lightSpeedOut;
+ animation-name: lightSpeedOut;
+ -webkit-animation-timing-function: ease-in;
+ animation-timing-function: ease-in;
+}
+
+@-webkit-keyframes rotateIn {
+ 0% {
+ -webkit-transform-origin: center;
+ transform-origin: center;
+ -webkit-transform: rotate3d(0, 0, 1, -200deg);
+ transform: rotate3d(0, 0, 1, -200deg);
+ opacity: 0;
+ }
+
+ 100% {
+ -webkit-transform-origin: center;
+ transform-origin: center;
+ -webkit-transform: none;
+ transform: none;
+ opacity: 1;
+ }
+}
+
+@keyframes rotateIn {
+ 0% {
+ -webkit-transform-origin: center;
+ transform-origin: center;
+ -webkit-transform: rotate3d(0, 0, 1, -200deg);
+ transform: rotate3d(0, 0, 1, -200deg);
+ opacity: 0;
+ }
+
+ 100% {
+ -webkit-transform-origin: center;
+ transform-origin: center;
+ -webkit-transform: none;
+ transform: none;
+ opacity: 1;
+ }
+}
+
+.rotateIn {
+ -webkit-animation-name: rotateIn;
+ animation-name: rotateIn;
+}
+
+@-webkit-keyframes rotateInDownLeft {
+ 0% {
+ -webkit-transform-origin: left bottom;
+ transform-origin: left bottom;
+ -webkit-transform: rotate3d(0, 0, 1, -45deg);
+ transform: rotate3d(0, 0, 1, -45deg);
+ opacity: 0;
+ }
+
+ 100% {
+ -webkit-transform-origin: left bottom;
+ transform-origin: left bottom;
+ -webkit-transform: none;
+ transform: none;
+ opacity: 1;
+ }
+}
+
+@keyframes rotateInDownLeft {
+ 0% {
+ -webkit-transform-origin: left bottom;
+ transform-origin: left bottom;
+ -webkit-transform: rotate3d(0, 0, 1, -45deg);
+ transform: rotate3d(0, 0, 1, -45deg);
+ opacity: 0;
+ }
+
+ 100% {
+ -webkit-transform-origin: left bottom;
+ transform-origin: left bottom;
+ -webkit-transform: none;
+ transform: none;
+ opacity: 1;
+ }
+}
+
+.rotateInDownLeft {
+ -webkit-animation-name: rotateInDownLeft;
+ animation-name: rotateInDownLeft;
+}
+
+@-webkit-keyframes rotateInDownRight {
+ 0% {
+ -webkit-transform-origin: right bottom;
+ transform-origin: right bottom;
+ -webkit-transform: rotate3d(0, 0, 1, 45deg);
+ transform: rotate3d(0, 0, 1, 45deg);
+ opacity: 0;
+ }
+
+ 100% {
+ -webkit-transform-origin: right bottom;
+ transform-origin: right bottom;
+ -webkit-transform: none;
+ transform: none;
+ opacity: 1;
+ }
+}
+
+@keyframes rotateInDownRight {
+ 0% {
+ -webkit-transform-origin: right bottom;
+ transform-origin: right bottom;
+ -webkit-transform: rotate3d(0, 0, 1, 45deg);
+ transform: rotate3d(0, 0, 1, 45deg);
+ opacity: 0;
+ }
+
+ 100% {
+ -webkit-transform-origin: right bottom;
+ transform-origin: right bottom;
+ -webkit-transform: none;
+ transform: none;
+ opacity: 1;
+ }
+}
+
+.rotateInDownRight {
+ -webkit-animation-name: rotateInDownRight;
+ animation-name: rotateInDownRight;
+}
+
+@-webkit-keyframes rotateInUpLeft {
+ 0% {
+ -webkit-transform-origin: left bottom;
+ transform-origin: left bottom;
+ -webkit-transform: rotate3d(0, 0, 1, 45deg);
+ transform: rotate3d(0, 0, 1, 45deg);
+ opacity: 0;
+ }
+
+ 100% {
+ -webkit-transform-origin: left bottom;
+ transform-origin: left bottom;
+ -webkit-transform: none;
+ transform: none;
+ opacity: 1;
+ }
+}
+
+@keyframes rotateInUpLeft {
+ 0% {
+ -webkit-transform-origin: left bottom;
+ transform-origin: left bottom;
+ -webkit-transform: rotate3d(0, 0, 1, 45deg);
+ transform: rotate3d(0, 0, 1, 45deg);
+ opacity: 0;
+ }
+
+ 100% {
+ -webkit-transform-origin: left bottom;
+ transform-origin: left bottom;
+ -webkit-transform: none;
+ transform: none;
+ opacity: 1;
+ }
+}
+
+.rotateInUpLeft {
+ -webkit-animation-name: rotateInUpLeft;
+ animation-name: rotateInUpLeft;
+}
+
+@-webkit-keyframes rotateInUpRight {
+ 0% {
+ -webkit-transform-origin: right bottom;
+ transform-origin: right bottom;
+ -webkit-transform: rotate3d(0, 0, 1, -90deg);
+ transform: rotate3d(0, 0, 1, -90deg);
+ opacity: 0;
+ }
+
+ 100% {
+ -webkit-transform-origin: right bottom;
+ transform-origin: right bottom;
+ -webkit-transform: none;
+ transform: none;
+ opacity: 1;
+ }
+}
+
+@keyframes rotateInUpRight {
+ 0% {
+ -webkit-transform-origin: right bottom;
+ transform-origin: right bottom;
+ -webkit-transform: rotate3d(0, 0, 1, -90deg);
+ transform: rotate3d(0, 0, 1, -90deg);
+ opacity: 0;
+ }
+
+ 100% {
+ -webkit-transform-origin: right bottom;
+ transform-origin: right bottom;
+ -webkit-transform: none;
+ transform: none;
+ opacity: 1;
+ }
+}
+
+.rotateInUpRight {
+ -webkit-animation-name: rotateInUpRight;
+ animation-name: rotateInUpRight;
+}
+
+@-webkit-keyframes rotateOut {
+ 0% {
+ -webkit-transform-origin: center;
+ transform-origin: center;
+ opacity: 1;
+ }
+
+ 100% {
+ -webkit-transform-origin: center;
+ transform-origin: center;
+ -webkit-transform: rotate3d(0, 0, 1, 200deg);
+ transform: rotate3d(0, 0, 1, 200deg);
+ opacity: 0;
+ }
+}
+
+@keyframes rotateOut {
+ 0% {
+ -webkit-transform-origin: center;
+ transform-origin: center;
+ opacity: 1;
+ }
+
+ 100% {
+ -webkit-transform-origin: center;
+ transform-origin: center;
+ -webkit-transform: rotate3d(0, 0, 1, 200deg);
+ transform: rotate3d(0, 0, 1, 200deg);
+ opacity: 0;
+ }
+}
+
+.rotateOut {
+ -webkit-animation-name: rotateOut;
+ animation-name: rotateOut;
+}
+
+@-webkit-keyframes rotateOutDownLeft {
+ 0% {
+ -webkit-transform-origin: left bottom;
+ transform-origin: left bottom;
+ opacity: 1;
+ }
+
+ 100% {
+ -webkit-transform-origin: left bottom;
+ transform-origin: left bottom;
+ -webkit-transform: rotate3d(0, 0, 1, 45deg);
+ transform: rotate3d(0, 0, 1, 45deg);
+ opacity: 0;
+ }
+}
+
+@keyframes rotateOutDownLeft {
+ 0% {
+ -webkit-transform-origin: left bottom;
+ transform-origin: left bottom;
+ opacity: 1;
+ }
+
+ 100% {
+ -webkit-transform-origin: left bottom;
+ transform-origin: left bottom;
+ -webkit-transform: rotate3d(0, 0, 1, 45deg);
+ transform: rotate3d(0, 0, 1, 45deg);
+ opacity: 0;
+ }
+}
+
+.rotateOutDownLeft {
+ -webkit-animation-name: rotateOutDownLeft;
+ animation-name: rotateOutDownLeft;
+}
+
+@-webkit-keyframes rotateOutDownRight {
+ 0% {
+ -webkit-transform-origin: right bottom;
+ transform-origin: right bottom;
+ opacity: 1;
+ }
+
+ 100% {
+ -webkit-transform-origin: right bottom;
+ transform-origin: right bottom;
+ -webkit-transform: rotate3d(0, 0, 1, -45deg);
+ transform: rotate3d(0, 0, 1, -45deg);
+ opacity: 0;
+ }
+}
+
+@keyframes rotateOutDownRight {
+ 0% {
+ -webkit-transform-origin: right bottom;
+ transform-origin: right bottom;
+ opacity: 1;
+ }
+
+ 100% {
+ -webkit-transform-origin: right bottom;
+ transform-origin: right bottom;
+ -webkit-transform: rotate3d(0, 0, 1, -45deg);
+ transform: rotate3d(0, 0, 1, -45deg);
+ opacity: 0;
+ }
+}
+
+.rotateOutDownRight {
+ -webkit-animation-name: rotateOutDownRight;
+ animation-name: rotateOutDownRight;
+}
+
+@-webkit-keyframes rotateOutUpLeft {
+ 0% {
+ -webkit-transform-origin: left bottom;
+ transform-origin: left bottom;
+ opacity: 1;
+ }
+
+ 100% {
+ -webkit-transform-origin: left bottom;
+ transform-origin: left bottom;
+ -webkit-transform: rotate3d(0, 0, 1, -45deg);
+ transform: rotate3d(0, 0, 1, -45deg);
+ opacity: 0;
+ }
+}
+
+@keyframes rotateOutUpLeft {
+ 0% {
+ -webkit-transform-origin: left bottom;
+ transform-origin: left bottom;
+ opacity: 1;
+ }
+
+ 100% {
+ -webkit-transform-origin: left bottom;
+ transform-origin: left bottom;
+ -webkit-transform: rotate3d(0, 0, 1, -45deg);
+ transform: rotate3d(0, 0, 1, -45deg);
+ opacity: 0;
+ }
+}
+
+.rotateOutUpLeft {
+ -webkit-animation-name: rotateOutUpLeft;
+ animation-name: rotateOutUpLeft;
+}
+
+@-webkit-keyframes rotateOutUpRight {
+ 0% {
+ -webkit-transform-origin: right bottom;
+ transform-origin: right bottom;
+ opacity: 1;
+ }
+
+ 100% {
+ -webkit-transform-origin: right bottom;
+ transform-origin: right bottom;
+ -webkit-transform: rotate3d(0, 0, 1, 90deg);
+ transform: rotate3d(0, 0, 1, 90deg);
+ opacity: 0;
+ }
+}
+
+@keyframes rotateOutUpRight {
+ 0% {
+ -webkit-transform-origin: right bottom;
+ transform-origin: right bottom;
+ opacity: 1;
+ }
+
+ 100% {
+ -webkit-transform-origin: right bottom;
+ transform-origin: right bottom;
+ -webkit-transform: rotate3d(0, 0, 1, 90deg);
+ transform: rotate3d(0, 0, 1, 90deg);
+ opacity: 0;
+ }
+}
+
+.rotateOutUpRight {
+ -webkit-animation-name: rotateOutUpRight;
+ animation-name: rotateOutUpRight;
+}
+
+@-webkit-keyframes hinge {
+ 0% {
+ -webkit-transform-origin: top left;
+ transform-origin: top left;
+ -webkit-animation-timing-function: ease-in-out;
+ animation-timing-function: ease-in-out;
+ }
+
+ 20%, 60% {
+ -webkit-transform: rotate3d(0, 0, 1, 80deg);
+ transform: rotate3d(0, 0, 1, 80deg);
+ -webkit-transform-origin: top left;
+ transform-origin: top left;
+ -webkit-animation-timing-function: ease-in-out;
+ animation-timing-function: ease-in-out;
+ }
+
+ 40%, 80% {
+ -webkit-transform: rotate3d(0, 0, 1, 60deg);
+ transform: rotate3d(0, 0, 1, 60deg);
+ -webkit-transform-origin: top left;
+ transform-origin: top left;
+ -webkit-animation-timing-function: ease-in-out;
+ animation-timing-function: ease-in-out;
+ opacity: 1;
+ }
+
+ 100% {
+ -webkit-transform: translate3d(0, 700px, 0);
+ transform: translate3d(0, 700px, 0);
+ opacity: 0;
+ }
+}
+
+@keyframes hinge {
+ 0% {
+ -webkit-transform-origin: top left;
+ transform-origin: top left;
+ -webkit-animation-timing-function: ease-in-out;
+ animation-timing-function: ease-in-out;
+ }
+
+ 20%, 60% {
+ -webkit-transform: rotate3d(0, 0, 1, 80deg);
+ transform: rotate3d(0, 0, 1, 80deg);
+ -webkit-transform-origin: top left;
+ transform-origin: top left;
+ -webkit-animation-timing-function: ease-in-out;
+ animation-timing-function: ease-in-out;
+ }
+
+ 40%, 80% {
+ -webkit-transform: rotate3d(0, 0, 1, 60deg);
+ transform: rotate3d(0, 0, 1, 60deg);
+ -webkit-transform-origin: top left;
+ transform-origin: top left;
+ -webkit-animation-timing-function: ease-in-out;
+ animation-timing-function: ease-in-out;
+ opacity: 1;
+ }
+
+ 100% {
+ -webkit-transform: translate3d(0, 700px, 0);
+ transform: translate3d(0, 700px, 0);
+ opacity: 0;
+ }
+}
+
+.hinge {
+ -webkit-animation-name: hinge;
+ animation-name: hinge;
+}
+
+/* originally authored by Nick Pettit - https://github.com/nickpettit/glide */
+
+@-webkit-keyframes rollIn {
+ 0% {
+ opacity: 0;
+ -webkit-transform: translate3d(-100%, 0, 0) rotate3d(0, 0, 1, -120deg);
+ transform: translate3d(-100%, 0, 0) rotate3d(0, 0, 1, -120deg);
+ }
+
+ 100% {
+ opacity: 1;
+ -webkit-transform: none;
+ transform: none;
+ }
+}
+
+@keyframes rollIn {
+ 0% {
+ opacity: 0;
+ -webkit-transform: translate3d(-100%, 0, 0) rotate3d(0, 0, 1, -120deg);
+ transform: translate3d(-100%, 0, 0) rotate3d(0, 0, 1, -120deg);
+ }
+
+ 100% {
+ opacity: 1;
+ -webkit-transform: none;
+ transform: none;
+ }
+}
+
+.rollIn {
+ -webkit-animation-name: rollIn;
+ animation-name: rollIn;
+}
+
+/* originally authored by Nick Pettit - https://github.com/nickpettit/glide */
+
+@-webkit-keyframes rollOut {
+ 0% {
+ opacity: 1;
+ }
+
+ 100% {
+ opacity: 0;
+ -webkit-transform: translate3d(100%, 0, 0) rotate3d(0, 0, 1, 120deg);
+ transform: translate3d(100%, 0, 0) rotate3d(0, 0, 1, 120deg);
+ }
+}
+
+@keyframes rollOut {
+ 0% {
+ opacity: 1;
+ }
+
+ 100% {
+ opacity: 0;
+ -webkit-transform: translate3d(100%, 0, 0) rotate3d(0, 0, 1, 120deg);
+ transform: translate3d(100%, 0, 0) rotate3d(0, 0, 1, 120deg);
+ }
+}
+
+.rollOut {
+ -webkit-animation-name: rollOut;
+ animation-name: rollOut;
+}
+
+@-webkit-keyframes zoomIn {
+ 0% {
+ opacity: 0;
+ -webkit-transform: scale3d(.3, .3, .3);
+ transform: scale3d(.3, .3, .3);
+ }
+
+ 50% {
+ opacity: 1;
+ }
+}
+
+@keyframes zoomIn {
+ 0% {
+ opacity: 0;
+ -webkit-transform: scale3d(.3, .3, .3);
+ transform: scale3d(.3, .3, .3);
+ }
+
+ 50% {
+ opacity: 1;
+ }
+}
+
+.zoomIn {
+ -webkit-animation-name: zoomIn;
+ animation-name: zoomIn;
+}
+
+@-webkit-keyframes zoomInDown {
+ 0% {
+ opacity: 0;
+ -webkit-transform: scale3d(.1, .1, .1) translate3d(0, -1000px, 0);
+ transform: scale3d(.1, .1, .1) translate3d(0, -1000px, 0);
+ -webkit-animation-timing-function: cubic-bezier(0.550, 0.055, 0.675, 0.190);
+ animation-timing-function: cubic-bezier(0.550, 0.055, 0.675, 0.190);
+ }
+
+ 60% {
+ opacity: 1;
+ -webkit-transform: scale3d(.475, .475, .475) translate3d(0, 60px, 0);
+ transform: scale3d(.475, .475, .475) translate3d(0, 60px, 0);
+ -webkit-animation-timing-function: cubic-bezier(0.175, 0.885, 0.320, 1);
+ animation-timing-function: cubic-bezier(0.175, 0.885, 0.320, 1);
+ }
+}
+
+@keyframes zoomInDown {
+ 0% {
+ opacity: 0;
+ -webkit-transform: scale3d(.1, .1, .1) translate3d(0, -1000px, 0);
+ transform: scale3d(.1, .1, .1) translate3d(0, -1000px, 0);
+ -webkit-animation-timing-function: cubic-bezier(0.550, 0.055, 0.675, 0.190);
+ animation-timing-function: cubic-bezier(0.550, 0.055, 0.675, 0.190);
+ }
+
+ 60% {
+ opacity: 1;
+ -webkit-transform: scale3d(.475, .475, .475) translate3d(0, 60px, 0);
+ transform: scale3d(.475, .475, .475) translate3d(0, 60px, 0);
+ -webkit-animation-timing-function: cubic-bezier(0.175, 0.885, 0.320, 1);
+ animation-timing-function: cubic-bezier(0.175, 0.885, 0.320, 1);
+ }
+}
+
+.zoomInDown {
+ -webkit-animation-name: zoomInDown;
+ animation-name: zoomInDown;
+}
+
+@-webkit-keyframes zoomInLeft {
+ 0% {
+ opacity: 0;
+ -webkit-transform: scale3d(.1, .1, .1) translate3d(-1000px, 0, 0);
+ transform: scale3d(.1, .1, .1) translate3d(-1000px, 0, 0);
+ -webkit-animation-timing-function: cubic-bezier(0.550, 0.055, 0.675, 0.190);
+ animation-timing-function: cubic-bezier(0.550, 0.055, 0.675, 0.190);
+ }
+
+ 60% {
+ opacity: 1;
+ -webkit-transform: scale3d(.475, .475, .475) translate3d(10px, 0, 0);
+ transform: scale3d(.475, .475, .475) translate3d(10px, 0, 0);
+ -webkit-animation-timing-function: cubic-bezier(0.175, 0.885, 0.320, 1);
+ animation-timing-function: cubic-bezier(0.175, 0.885, 0.320, 1);
+ }
+}
+
+@keyframes zoomInLeft {
+ 0% {
+ opacity: 0;
+ -webkit-transform: scale3d(.1, .1, .1) translate3d(-1000px, 0, 0);
+ transform: scale3d(.1, .1, .1) translate3d(-1000px, 0, 0);
+ -webkit-animation-timing-function: cubic-bezier(0.550, 0.055, 0.675, 0.190);
+ animation-timing-function: cubic-bezier(0.550, 0.055, 0.675, 0.190);
+ }
+
+ 60% {
+ opacity: 1;
+ -webkit-transform: scale3d(.475, .475, .475) translate3d(10px, 0, 0);
+ transform: scale3d(.475, .475, .475) translate3d(10px, 0, 0);
+ -webkit-animation-timing-function: cubic-bezier(0.175, 0.885, 0.320, 1);
+ animation-timing-function: cubic-bezier(0.175, 0.885, 0.320, 1);
+ }
+}
+
+.zoomInLeft {
+ -webkit-animation-name: zoomInLeft;
+ animation-name: zoomInLeft;
+}
+
+@-webkit-keyframes zoomInRight {
+ 0% {
+ opacity: 0;
+ -webkit-transform: scale3d(.1, .1, .1) translate3d(1000px, 0, 0);
+ transform: scale3d(.1, .1, .1) translate3d(1000px, 0, 0);
+ -webkit-animation-timing-function: cubic-bezier(0.550, 0.055, 0.675, 0.190);
+ animation-timing-function: cubic-bezier(0.550, 0.055, 0.675, 0.190);
+ }
+
+ 60% {
+ opacity: 1;
+ -webkit-transform: scale3d(.475, .475, .475) translate3d(-10px, 0, 0);
+ transform: scale3d(.475, .475, .475) translate3d(-10px, 0, 0);
+ -webkit-animation-timing-function: cubic-bezier(0.175, 0.885, 0.320, 1);
+ animation-timing-function: cubic-bezier(0.175, 0.885, 0.320, 1);
+ }
+}
+
+@keyframes zoomInRight {
+ 0% {
+ opacity: 0;
+ -webkit-transform: scale3d(.1, .1, .1) translate3d(1000px, 0, 0);
+ transform: scale3d(.1, .1, .1) translate3d(1000px, 0, 0);
+ -webkit-animation-timing-function: cubic-bezier(0.550, 0.055, 0.675, 0.190);
+ animation-timing-function: cubic-bezier(0.550, 0.055, 0.675, 0.190);
+ }
+
+ 60% {
+ opacity: 1;
+ -webkit-transform: scale3d(.475, .475, .475) translate3d(-10px, 0, 0);
+ transform: scale3d(.475, .475, .475) translate3d(-10px, 0, 0);
+ -webkit-animation-timing-function: cubic-bezier(0.175, 0.885, 0.320, 1);
+ animation-timing-function: cubic-bezier(0.175, 0.885, 0.320, 1);
+ }
+}
+
+.zoomInRight {
+ -webkit-animation-name: zoomInRight;
+ animation-name: zoomInRight;
+}
+
+@-webkit-keyframes zoomInUp {
+ 0% {
+ opacity: 0;
+ -webkit-transform: scale3d(.1, .1, .1) translate3d(0, 1000px, 0);
+ transform: scale3d(.1, .1, .1) translate3d(0, 1000px, 0);
+ -webkit-animation-timing-function: cubic-bezier(0.550, 0.055, 0.675, 0.190);
+ animation-timing-function: cubic-bezier(0.550, 0.055, 0.675, 0.190);
+ }
+
+ 60% {
+ opacity: 1;
+ -webkit-transform: scale3d(.475, .475, .475) translate3d(0, -60px, 0);
+ transform: scale3d(.475, .475, .475) translate3d(0, -60px, 0);
+ -webkit-animation-timing-function: cubic-bezier(0.175, 0.885, 0.320, 1);
+ animation-timing-function: cubic-bezier(0.175, 0.885, 0.320, 1);
+ }
+}
+
+@keyframes zoomInUp {
+ 0% {
+ opacity: 0;
+ -webkit-transform: scale3d(.1, .1, .1) translate3d(0, 1000px, 0);
+ transform: scale3d(.1, .1, .1) translate3d(0, 1000px, 0);
+ -webkit-animation-timing-function: cubic-bezier(0.550, 0.055, 0.675, 0.190);
+ animation-timing-function: cubic-bezier(0.550, 0.055, 0.675, 0.190);
+ }
+
+ 60% {
+ opacity: 1;
+ -webkit-transform: scale3d(.475, .475, .475) translate3d(0, -60px, 0);
+ transform: scale3d(.475, .475, .475) translate3d(0, -60px, 0);
+ -webkit-animation-timing-function: cubic-bezier(0.175, 0.885, 0.320, 1);
+ animation-timing-function: cubic-bezier(0.175, 0.885, 0.320, 1);
+ }
+}
+
+.zoomInUp {
+ -webkit-animation-name: zoomInUp;
+ animation-name: zoomInUp;
+}
+
+@-webkit-keyframes zoomOut {
+ 0% {
+ opacity: 1;
+ }
+
+ 50% {
+ opacity: 0;
+ -webkit-transform: scale3d(.3, .3, .3);
+ transform: scale3d(.3, .3, .3);
+ }
+
+ 100% {
+ opacity: 0;
+ }
+}
+
+@keyframes zoomOut {
+ 0% {
+ opacity: 1;
+ }
+
+ 50% {
+ opacity: 0;
+ -webkit-transform: scale3d(.3, .3, .3);
+ transform: scale3d(.3, .3, .3);
+ }
+
+ 100% {
+ opacity: 0;
+ }
+}
+
+.zoomOut {
+ -webkit-animation-name: zoomOut;
+ animation-name: zoomOut;
+}
+
+@-webkit-keyframes zoomOutDown {
+ 40% {
+ opacity: 1;
+ -webkit-transform: scale3d(.475, .475, .475) translate3d(0, -60px, 0);
+ transform: scale3d(.475, .475, .475) translate3d(0, -60px, 0);
+ -webkit-animation-timing-function: cubic-bezier(0.550, 0.055, 0.675, 0.190);
+ animation-timing-function: cubic-bezier(0.550, 0.055, 0.675, 0.190);
+ }
+
+ 100% {
+ opacity: 0;
+ -webkit-transform: scale3d(.1, .1, .1) translate3d(0, 2000px, 0);
+ transform: scale3d(.1, .1, .1) translate3d(0, 2000px, 0);
+ -webkit-transform-origin: center bottom;
+ transform-origin: center bottom;
+ -webkit-animation-timing-function: cubic-bezier(0.175, 0.885, 0.320, 1);
+ animation-timing-function: cubic-bezier(0.175, 0.885, 0.320, 1);
+ }
+}
+
+@keyframes zoomOutDown {
+ 40% {
+ opacity: 1;
+ -webkit-transform: scale3d(.475, .475, .475) translate3d(0, -60px, 0);
+ transform: scale3d(.475, .475, .475) translate3d(0, -60px, 0);
+ -webkit-animation-timing-function: cubic-bezier(0.550, 0.055, 0.675, 0.190);
+ animation-timing-function: cubic-bezier(0.550, 0.055, 0.675, 0.190);
+ }
+
+ 100% {
+ opacity: 0;
+ -webkit-transform: scale3d(.1, .1, .1) translate3d(0, 2000px, 0);
+ transform: scale3d(.1, .1, .1) translate3d(0, 2000px, 0);
+ -webkit-transform-origin: center bottom;
+ transform-origin: center bottom;
+ -webkit-animation-timing-function: cubic-bezier(0.175, 0.885, 0.320, 1);
+ animation-timing-function: cubic-bezier(0.175, 0.885, 0.320, 1);
+ }
+}
+
+.zoomOutDown {
+ -webkit-animation-name: zoomOutDown;
+ animation-name: zoomOutDown;
+}
+
+@-webkit-keyframes zoomOutLeft {
+ 40% {
+ opacity: 1;
+ -webkit-transform: scale3d(.475, .475, .475) translate3d(42px, 0, 0);
+ transform: scale3d(.475, .475, .475) translate3d(42px, 0, 0);
+ }
+
+ 100% {
+ opacity: 0;
+ -webkit-transform: scale(.1) translate3d(-2000px, 0, 0);
+ transform: scale(.1) translate3d(-2000px, 0, 0);
+ -webkit-transform-origin: left center;
+ transform-origin: left center;
+ }
+}
+
+@keyframes zoomOutLeft {
+ 40% {
+ opacity: 1;
+ -webkit-transform: scale3d(.475, .475, .475) translate3d(42px, 0, 0);
+ transform: scale3d(.475, .475, .475) translate3d(42px, 0, 0);
+ }
+
+ 100% {
+ opacity: 0;
+ -webkit-transform: scale(.1) translate3d(-2000px, 0, 0);
+ transform: scale(.1) translate3d(-2000px, 0, 0);
+ -webkit-transform-origin: left center;
+ transform-origin: left center;
+ }
+}
+
+.zoomOutLeft {
+ -webkit-animation-name: zoomOutLeft;
+ animation-name: zoomOutLeft;
+}
+
+@-webkit-keyframes zoomOutRight {
+ 40% {
+ opacity: 1;
+ -webkit-transform: scale3d(.475, .475, .475) translate3d(-42px, 0, 0);
+ transform: scale3d(.475, .475, .475) translate3d(-42px, 0, 0);
+ }
+
+ 100% {
+ opacity: 0;
+ -webkit-transform: scale(.1) translate3d(2000px, 0, 0);
+ transform: scale(.1) translate3d(2000px, 0, 0);
+ -webkit-transform-origin: right center;
+ transform-origin: right center;
+ }
+}
+
+@keyframes zoomOutRight {
+ 40% {
+ opacity: 1;
+ -webkit-transform: scale3d(.475, .475, .475) translate3d(-42px, 0, 0);
+ transform: scale3d(.475, .475, .475) translate3d(-42px, 0, 0);
+ }
+
+ 100% {
+ opacity: 0;
+ -webkit-transform: scale(.1) translate3d(2000px, 0, 0);
+ transform: scale(.1) translate3d(2000px, 0, 0);
+ -webkit-transform-origin: right center;
+ transform-origin: right center;
+ }
+}
+
+.zoomOutRight {
+ -webkit-animation-name: zoomOutRight;
+ animation-name: zoomOutRight;
+}
+
+@-webkit-keyframes zoomOutUp {
+ 40% {
+ opacity: 1;
+ -webkit-transform: scale3d(.475, .475, .475) translate3d(0, 60px, 0);
+ transform: scale3d(.475, .475, .475) translate3d(0, 60px, 0);
+ -webkit-animation-timing-function: cubic-bezier(0.550, 0.055, 0.675, 0.190);
+ animation-timing-function: cubic-bezier(0.550, 0.055, 0.675, 0.190);
+ }
+
+ 100% {
+ opacity: 0;
+ -webkit-transform: scale3d(.1, .1, .1) translate3d(0, -2000px, 0);
+ transform: scale3d(.1, .1, .1) translate3d(0, -2000px, 0);
+ -webkit-transform-origin: center bottom;
+ transform-origin: center bottom;
+ -webkit-animation-timing-function: cubic-bezier(0.175, 0.885, 0.320, 1);
+ animation-timing-function: cubic-bezier(0.175, 0.885, 0.320, 1);
+ }
+}
+
+@keyframes zoomOutUp {
+ 40% {
+ opacity: 1;
+ -webkit-transform: scale3d(.475, .475, .475) translate3d(0, 60px, 0);
+ transform: scale3d(.475, .475, .475) translate3d(0, 60px, 0);
+ -webkit-animation-timing-function: cubic-bezier(0.550, 0.055, 0.675, 0.190);
+ animation-timing-function: cubic-bezier(0.550, 0.055, 0.675, 0.190);
+ }
+
+ 100% {
+ opacity: 0;
+ -webkit-transform: scale3d(.1, .1, .1) translate3d(0, -2000px, 0);
+ transform: scale3d(.1, .1, .1) translate3d(0, -2000px, 0);
+ -webkit-transform-origin: center bottom;
+ transform-origin: center bottom;
+ -webkit-animation-timing-function: cubic-bezier(0.175, 0.885, 0.320, 1);
+ animation-timing-function: cubic-bezier(0.175, 0.885, 0.320, 1);
+ }
+}
+
+.zoomOutUp {
+ -webkit-animation-name: zoomOutUp;
+ animation-name: zoomOutUp;
+}
+
+@-webkit-keyframes slideInDown {
+ 0% {
+ -webkit-transform: translateY(-100%);
+ transform: translateY(-100%);
+ visibility: visible;
+ }
+
+ 100% {
+ -webkit-transform: translateY(0);
+ transform: translateY(0);
+ }
+}
+
+@keyframes slideInDown {
+ 0% {
+ -webkit-transform: translateY(-100%);
+ transform: translateY(-100%);
+ visibility: visible;
+ }
+
+ 100% {
+ -webkit-transform: translateY(0);
+ transform: translateY(0);
+ }
+}
+
+.slideInDown {
+ -webkit-animation-name: slideInDown;
+ animation-name: slideInDown;
+}
+
+@-webkit-keyframes slideInLeft {
+ 0% {
+ -webkit-transform: translateX(-100%);
+ transform: translateX(-100%);
+ visibility: visible;
+ }
+
+ 100% {
+ -webkit-transform: translateX(0);
+ transform: translateX(0);
+ }
+}
+
+@keyframes slideInLeft {
+ 0% {
+ -webkit-transform: translateX(-100%);
+ transform: translateX(-100%);
+ visibility: visible;
+ }
+
+ 100% {
+ -webkit-transform: translateX(0);
+ transform: translateX(0);
+ }
+}
+
+.slideInLeft {
+ -webkit-animation-name: slideInLeft;
+ animation-name: slideInLeft;
+}
+
+@-webkit-keyframes slideInRight {
+ 0% {
+ -webkit-transform: translateX(100%);
+ transform: translateX(100%);
+ visibility: visible;
+ }
+
+ 100% {
+ -webkit-transform: translateX(0);
+ transform: translateX(0);
+ }
+}
+
+@keyframes slideInRight {
+ 0% {
+ -webkit-transform: translateX(100%);
+ transform: translateX(100%);
+ visibility: visible;
+ }
+
+ 100% {
+ -webkit-transform: translateX(0);
+ transform: translateX(0);
+ }
+}
+
+.slideInRight {
+ -webkit-animation-name: slideInRight;
+ animation-name: slideInRight;
+}
+
+@-webkit-keyframes slideInUp {
+ 0% {
+ -webkit-transform: translateY(100%);
+ transform: translateY(100%);
+ visibility: visible;
+ }
+
+ 100% {
+ -webkit-transform: translateY(0);
+ transform: translateY(0);
+ }
+}
+
+@keyframes slideInUp {
+ 0% {
+ -webkit-transform: translateY(100%);
+ transform: translateY(100%);
+ visibility: visible;
+ }
+
+ 100% {
+ -webkit-transform: translateY(0);
+ transform: translateY(0);
+ }
+}
+
+.slideInUp {
+ -webkit-animation-name: slideInUp;
+ animation-name: slideInUp;
+}
+
+@-webkit-keyframes slideOutDown {
+ 0% {
+ -webkit-transform: translateY(0);
+ transform: translateY(0);
+ }
+
+ 100% {
+ visibility: hidden;
+ -webkit-transform: translateY(100%);
+ transform: translateY(100%);
+ }
+}
+
+@keyframes slideOutDown {
+ 0% {
+ -webkit-transform: translateY(0);
+ transform: translateY(0);
+ }
+
+ 100% {
+ visibility: hidden;
+ -webkit-transform: translateY(100%);
+ transform: translateY(100%);
+ }
+}
+
+.slideOutDown {
+ -webkit-animation-name: slideOutDown;
+ animation-name: slideOutDown;
+}
+
+@-webkit-keyframes slideOutLeft {
+ 0% {
+ -webkit-transform: translateX(0);
+ transform: translateX(0);
+ }
+
+ 100% {
+ visibility: hidden;
+ -webkit-transform: translateX(-100%);
+ transform: translateX(-100%);
+ }
+}
+
+@keyframes slideOutLeft {
+ 0% {
+ -webkit-transform: translateX(0);
+ transform: translateX(0);
+ }
+
+ 100% {
+ visibility: hidden;
+ -webkit-transform: translateX(-100%);
+ transform: translateX(-100%);
+ }
+}
+
+.slideOutLeft {
+ -webkit-animation-name: slideOutLeft;
+ animation-name: slideOutLeft;
+}
+
+@-webkit-keyframes slideOutRight {
+ 0% {
+ -webkit-transform: translateX(0);
+ transform: translateX(0);
+ }
+
+ 100% {
+ visibility: hidden;
+ -webkit-transform: translateX(100%);
+ transform: translateX(100%);
+ }
+}
+
+@keyframes slideOutRight {
+ 0% {
+ -webkit-transform: translateX(0);
+ transform: translateX(0);
+ }
+
+ 100% {
+ visibility: hidden;
+ -webkit-transform: translateX(100%);
+ transform: translateX(100%);
+ }
+}
+
+.slideOutRight {
+ -webkit-animation-name: slideOutRight;
+ animation-name: slideOutRight;
+}
+
+@-webkit-keyframes slideOutUp {
+ 0% {
+ -webkit-transform: translateY(0);
+ transform: translateY(0);
+ }
+
+ 100% {
+ visibility: hidden;
+ -webkit-transform: translateY(-100%);
+ transform: translateY(-100%);
+ }
+}
+
+@keyframes slideOutUp {
+ 0% {
+ -webkit-transform: translateY(0);
+ transform: translateY(0);
+ }
+
+ 100% {
+ visibility: hidden;
+ -webkit-transform: translateY(-100%);
+ transform: translateY(-100%);
+ }
+}
+
+.slideOutUp {
+ -webkit-animation-name: slideOutUp;
+ animation-name: slideOutUp;
+}
diff --git a/git_deps/html/css/git-deps-tips.css b/git_deps/html/css/git-deps-tips.css
new file mode 100644
index 0000000..909badb
--- /dev/null
+++ b/git_deps/html/css/git-deps-tips.css
@@ -0,0 +1,79 @@
+.d3-tip {
+ line-height: 1;
+ padding: 5px;
+ background: rgba(247, 251, 252, 0.9);
+ font-family: Helvetica, arial, freesans, clean, sans-serif, 'Segoe UI Emoji', 'Segoe UI Symbol';
+ border-radius: 2px;
+ pointer-events: none;
+ border: 1px solid #e5e5e5;
+}
+
+/* Creates a small triangle extender for the tooltip */
+.d3-tip:after {
+ box-sizing: border-box;
+ display: inline;
+ font-size: 10px;
+ width: 100%;
+ line-height: 1;
+ color: rgba(0, 0, 0, 0.8);
+ position: absolute;
+ pointer-events: none;
+}
+
+/* Northward tooltips */
+.d3-tip.n:after {
+ content: "\25BC";
+ margin: -1px 0 0 0;
+ top: 100%;
+ left: 0;
+ text-align: center;
+}
+
+/* Eastward tooltips */
+.d3-tip.e:after {
+ content: "\25C0";
+ margin: -4px 0 0 0;
+ top: 50%;
+ left: -8px;
+}
+
+/* Southward tooltips */
+.d3-tip.s:after {
+ content: "\25B2";
+ margin: 0 0 1px 0;
+ top: -8px;
+ left: 0;
+ text-align: center;
+}
+
+/* Westward tooltips */
+.d3-tip.w:after {
+ content: "\25B6";
+ margin: -4px 0 0 -1px;
+ top: 50%;
+ left: 100%;
+}
+
+.d3-tip p.commit-title {
+ font-weight: bold;
+ color: #4e575b;
+ font-size: 15px;
+ margin: 0.5em 0;
+}
+
+.d3-tip .commit-describe {
+ font-size: 12px;
+ margin: 0.5em 0;
+}
+
+.d3-tip .commit-meta {
+ color: #979a9c;
+ font-size: 11px;
+}
+
+.d3-tip .commit-body pre {
+ color: #596063;
+ margin: 0.5em 0;
+ /* padding-left: 8px; */
+ /* border-left: 1px solid #e5e5e5; */
+}
diff --git a/git_deps/html/css/git-deps.css b/git_deps/html/css/git-deps.css
new file mode 100644
index 0000000..ea21821
--- /dev/null
+++ b/git_deps/html/css/git-deps.css
@@ -0,0 +1,93 @@
+body {
+ font-family: Helvetica, arial, freesans, clean, sans-serif, 'Segoe UI Emoji', 'Segoe UI Symbol';
+ width: 100vw;
+ height: 100vh;
+ margin: 0px;
+}
+
+#page {
+ margin: 8px;
+ display: flex; /* use the flex model */
+ flex-direction: column;
+ position: absolute;
+ top: 0;
+ bottom: 0;
+ left: 0;
+ right: 0;
+}
+
+h1 {
+ margin: 0;
+ background: rgb(128, 150, 174);
+ color: white;
+ padding: 0.3em;
+}
+
+#top p {
+ margin-right: 320px; /* Avoid overlap with noty boxes */
+}
+
+#svg-container {
+ flex: 1;
+ border: 1px solid #ccc; /* width has to be half of SVG_MARGIN */
+}
+
+rect.background {
+ fill: white;
+ cursor: all-scroll;
+}
+
+g.node rect {
+ stroke: #e5e5e5;
+ stroke-width: 2px;
+ cursor: pointer; /* move is semantically better but looks the same as all-scroll */
+}
+
+g.node rect.explored {
+ fill: rgba(206, 236, 221, 0.54);
+}
+
+g.node rect.unexplored {
+ fill: rgba(242, 242, 255, 0.54);
+}
+
+g.node text {
+ /* fill: black; */
+ fill: #295b8c;
+ font-size: 15px;
+ font-family: Consolas, "Liberation Mono", Menlo, Courier, monospace;
+ font-weight: bold;
+ text-anchor: middle;
+ alignment-baseline: middle;
+ cursor: pointer;
+ pointer-events: none;
+}
+
+.plus-icon use {
+ display: none;
+}
+
+.plus-icon:hover use {
+ display: visible;
+}
+
+.link {
+ fill: none;
+ stroke-width: 2px;
+ opacity: 0.4;
+ marker-end: url(#end-arrow);
+}
+
+.commitish input {
+ font-family: Consolas, "Liberation Mono", Menlo, Courier, monospace;
+}
+
+.commit-ref {
+ font-weight: bold;
+ color: #26894d;
+}
+
+.noty_text p {
+ margin-top: 6px;
+ margin-bottom: 6px;
+}
diff --git a/git_deps/html/git-deps.html b/git_deps/html/git-deps.html
new file mode 100644
index 0000000..6ced12b
--- /dev/null
+++ b/git_deps/html/git-deps.html
@@ -0,0 +1,40 @@
+<!DOCTYPE html>
+
+<html lang="en">
+<head>
+ <meta charset="utf-8" />
+ <title>git commit dependency graph</title>
+
+ <script language="javascript" type="text/javascript" src="js/bundle.js"></script>
+
+ <link rel="stylesheet" type="text/css" href="css/animate.css" />
+ <link rel="stylesheet" type="text/css" href="css/git-deps.css" />
+ <link rel="stylesheet" type="text/css" href="css/git-deps-tips.css" />
+</head>
+
+<body>
+ <div id="page">
+ <div id="top">
+ <h1>git commit dependency graph</h1>
+
+ <p>
+ Use mouse-wheel to zoom.
+ Drag background to pan.
+ Hover over a commit for more information.
+ Click a commit's plus icon to find dependencies of that commit.
+ </p>
+
+ <form class="commitish" action="#">
+ Detect dependencies for:
+ <input type="text" name="commitish" size="20"
+ value="master" autofocus />
+ <button>Submit</button>
+ <button type="button" onclick="full_screen_click()">Full screen</button>
+ <button type="button" onclick="zoom_to_fit()">Zoom to fit (or double click)</button>
+ </form>
+ </div>
+
+ <div id="svg-container" />
+ </div>
+</body>
+</html>
diff --git a/git_deps/html/js/.gitignore b/git_deps/html/js/.gitignore
new file mode 100644
index 0000000..0e804e3
--- /dev/null
+++ b/git_deps/html/js/.gitignore
@@ -0,0 +1 @@
+bundle.js
diff --git a/git_deps/html/js/fullscreen.js b/git_deps/html/js/fullscreen.js
new file mode 100644
index 0000000..6d8f3d8
--- /dev/null
+++ b/git_deps/html/js/fullscreen.js
@@ -0,0 +1,48 @@
+
+function endFullScreen(oncancel) {
+ if (!RunPrefixMethod(document, "FullScreen") && !RunPrefixMethod(document, "IsFullScreen")) {
+ oncancel();
+ }
+}
+function fullScreen(e, oncancel) {
+ if (RunPrefixMethod(document, "FullScreen") || RunPrefixMethod(document, "IsFullScreen")) {
+ RunPrefixMethod(document, "CancelFullScreen");
+ }
+ else {
+ RunPrefixMethod(e, "RequestFullScreen");
+ e.setAttribute("width", screen.width);
+ e.setAttribute("height", screen.height);
+ }
+ if (arguments.length > 1) {
+ var f = function () { endFullScreen(oncancel); };
+ document.addEventListener("fullscreenchange", f, false);
+ document.addEventListener("mozfullscreenchange", f, false);
+ document.addEventListener("webkitfullscreenchange", f, false);
+ }
+}
+
+var pfx = ["webkit", "moz", "ms", "o", ""];
+function RunPrefixMethod(obj, method) {
+
+ var p = 0, m, t;
+ while (p < pfx.length && !obj[m]) {
+ m = method;
+ if (pfx[p] == "") {
+ m = m.substr(0, 1).toLowerCase() + m.substr(1);
+ }
+ m = pfx[p] + m;
+ t = typeof obj[m];
+ if (t != "undefined") {
+ pfx = [pfx[p]];
+ return (t == "function" ? obj[m]() : obj[m]);
+ }
+ p++;
+ }
+}
+
+function isFullScreen() {
+ var fullscreenEnabled = document.fullscreenEnabled || document.mozFullScreenEnabled || document.webkitFullscreenEnabled;
+ return fullscreenEnabled;
+}
+
+module.exports = fullScreen;
diff --git a/git_deps/html/js/git-deps-data.coffee b/git_deps/html/js/git-deps-data.coffee
new file mode 100644
index 0000000..34715a3
--- /dev/null
+++ b/git_deps/html/js/git-deps-data.coffee
@@ -0,0 +1,108 @@
+# The list of nodes and links to feed into WebCola.
+# These will be dynamically built as we retrieve them via XHR.
+nodes = []
+links = []
+
+# WebCola requires links to refer to nodes by index within the
+# nodes array, so as nodes are dynamically added, we need to
+# be able to retrieve their index efficiently in order to add
+# links to/from them. This also allows us to avoid adding the
+# same node twice.
+node_index = {}
+
+# Track dependencies in a hash of hashes which maps parents to
+# children to booleans. Constraints will be added to try to keep
+# siblings at the same y position. For this we need to track
+# siblings, which we do by mapping each parent to an array of its
+# siblings in this hash. It also enables us to deduplicate links
+# across multiple XHRs.
+deps = {}
+
+# Track dependences in reverse in a hash of hashes which maps children
+# to parents to booleans. This allows us to highlight parents when
+# the mouse hovers over a child, and know when we can safely remove
+# a commit due to its sole parent being deleted.
+rdeps = {}
+
+# Returns 1 iff a node was added, otherwise 0.
+add_node = (commit) ->
+ if commit.sha1 of node_index
+ n = node commit.sha1
+ n.explored ||= commit.explored
+ return 0
+
+ nodes.push commit
+ node_index[commit.sha1] = nodes.length - 1
+ return 1
+
+# Returns 1 iff a dependency was added, otherwise 0.
+add_dependency = (parent_sha1, child_sha1) ->
+ deps[parent_sha1] = {} unless parent_sha1 of deps
+
+ # We've already got this link, presumably
+ # from a previous XHR.
+ return 0 if child_sha1 of deps[parent_sha1]
+ deps[parent_sha1][child_sha1] = true
+ add_link parent_sha1, child_sha1
+ return 1
+
+# Returns 1 iff a reverse dependency was added, otherwise 0.
+add_rev_dependency = (child_sha1, parent_sha1) ->
+ rdeps[child_sha1] = {} unless child_sha1 of rdeps
+
+ # We've already got this link, presumably
+ # from a previous XHR.
+ return 0 if parent_sha1 of rdeps[child_sha1]
+ rdeps[child_sha1][parent_sha1] = true
+ return 1
+
+add_link = (parent_sha1, child_sha1) ->
+ pi = node_index[parent_sha1]
+ ci = node_index[child_sha1]
+ link =
+ source: pi
+ target: ci
+ value: 1 # no idea what WebCola needs this for
+
+ links.push link
+ return
+
+# Returns true iff new data was added.
+add_data = (data) ->
+ new_nodes = 0
+ new_deps = 0
+ for commit in data.commits
+ new_nodes += add_node(commit)
+
+ for dep in data.dependencies
+ new_deps += add_dependency(dep.parent, dep.child)
+ add_rev_dependency(dep.child, dep.parent)
+
+ if new_nodes > 0 or new_deps > 0
+ return [
+ new_nodes
+ new_deps
+ data.query
+ ]
+
+ return false
+
+node = (sha1) ->
+ i = node_index[sha1]
+ unless i?
+ console.error "No index for SHA1 '#{sha1}'"
+ return null
+ return nodes[i]
+
+module.exports =
+ # Variables (N.B. if these variables are reinitialised at any
+ # point, the values here will become stale and require updating)
+ nodes: nodes
+ links: links
+ node_index: node_index
+ deps: deps
+ rdeps: rdeps
+
+ # Functions
+ add: add_data
+ node: node
diff --git a/git_deps/html/js/git-deps-graph.coffee b/git_deps/html/js/git-deps-graph.coffee
new file mode 100644
index 0000000..7ad6827
--- /dev/null
+++ b/git_deps/html/js/git-deps-graph.coffee
@@ -0,0 +1,595 @@
+jQuery = require "jquery"
+$ = jQuery
+d3 = require "d3"
+d3tip = require "d3-tip"
+d3tip d3
+
+# Hacky workaround:
+# https://github.com/tgdwyer/WebCola/issues/145#issuecomment-271316856
+window.d3 = d3
+
+cola = require "webcola"
+
+global.gdn = require "./git-deps-noty.coffee"
+global.gdd = require "./git-deps-data.coffee"
+global.gdl = require "./git-deps-layout.coffee"
+
+fullScreen = require "./fullscreen"
+
+SVG_MARGIN = 2 # space around <svg>, matching #svg-container border
+RECT_MARGIN = 14 # space in between <rects>
+PADDING = 5 # space in between <text> label and <rect> border
+EDGE_ROUTING_MARGIN = 3
+PLUS_ICON_WIDTH = 14
+
+svg_width = 960
+svg_height = 800
+old_svg_height = undefined
+old_svg_width = undefined
+
+color = d3.scale.category20()
+
+global.d3cola = cola.d3adaptor()
+d3cola
+ .flowLayout("y", 100)
+ .avoidOverlaps(true)
+ #.linkDistance(60)
+ #.symmetricDiffLinkLengths(30)
+ #.jaccardLinkLengths(100)
+
+# d3 visualization elements
+container = undefined
+svg = undefined
+fg = undefined
+nodes = undefined
+paths = undefined
+tip = undefined
+tip_template = undefined
+zoom = undefined
+
+options = undefined # Options will be retrieved from web server
+
+jQuery ->
+ d3.json "options", (error, data) ->
+ options = data
+ gdl.debug = options.debug
+
+ d3.html "tip-template.html", (error, html) ->
+ tip_template = html
+
+ #setup_default_form_values();
+ $("form.commitish").submit (event) ->
+ event.preventDefault()
+ add_commitish $(".commitish input").val()
+
+ init_svg()
+
+setup_default_form_values = ->
+ $("input[type=text]").each(->
+ $(this).val $(this).attr("defaultValue")
+ $(this).css color: "grey"
+ ).focus(->
+ if $(this).val() is $(this).attr("defaultValue")
+ $(this).val ""
+ $(this).css color: "black"
+ ).blur ->
+ if $(this).val() is ""
+ $(this).val $(this).attr("defaultValue")
+ $(this).css color: "grey"
+
+resize_window = ->
+ calculate_svg_size_from_container()
+ fit_svg_to_container()
+ redraw true
+
+redraw = (transition) ->
+ # if mouse down then we are dragging not panning
+ # if nodeMouseDown
+ # return
+ ((if transition then fg.transition() else fg))
+ .attr "transform",
+ "translate(#{zoom.translate()}) scale(#{zoom.scale()})"
+
+graph_bounds = ->
+ x = Number.POSITIVE_INFINITY
+ X = Number.NEGATIVE_INFINITY
+ y = Number.POSITIVE_INFINITY
+ Y = Number.NEGATIVE_INFINITY
+ fg.selectAll(".node").each (d) ->
+ x = Math.min(x, d.x - d.width / 2)
+ y = Math.min(y, d.y - d.height / 2)
+ X = Math.max(X, d.x + d.width / 2)
+ Y = Math.max(Y, d.y + d.height / 2)
+ return {} =
+ x: x
+ X: X
+ y: y
+ Y: Y
+
+fit_svg_to_container = ->
+ svg.attr("width", svg_width).attr("height", svg_height)
+
+full_screen_cancel = ->
+ svg_width = old_svg_width
+ svg_height = old_svg_height
+ fit_svg_to_container()
+ #zoom_to_fit();
+ resize_window()
+
+full_screen_click = ->
+ fullScreen container.node(), full_screen_cancel
+ fit_svg_to_container()
+ resize_window()
+ #zoom_to_fit();
+ return false
+
+zoom_to_fit = ->
+ b = graph_bounds()
+ w = b.X - b.x
+ h = b.Y - b.y
+ cw = svg.attr("width")
+ ch = svg.attr("height")
+ s = Math.min(cw / w, ch / h)
+ tx = -b.x * s + (cw / s - w) * s / 2
+ ty = -b.y * s + (ch / s - h) * s / 2
+ zoom.translate([tx, ty]).scale s
+ redraw true
+ return false
+
+window.full_screen_click = full_screen_click
+window.zoom_to_fit = zoom_to_fit
+
+add_commitish = (commitish) ->
+ tip.hide() if tip?
+ draw_graph commitish
+
+calculate_svg_size_from_container = ->
+ old_svg_width = svg_width
+ old_svg_height = svg_height
+ svg_width = container.node().offsetWidth - SVG_MARGIN
+ svg_height = container.node().offsetHeight - SVG_MARGIN
+
+init_svg = ->
+ container = d3.select("#svg-container")
+ calculate_svg_size_from_container()
+ svg = container.append("svg")
+ .attr("width", svg_width)
+ .attr("height", svg_height)
+ d3cola.size [svg_width, svg_height]
+
+ d3.select(window).on "resize", resize_window
+
+ zoom = d3.behavior.zoom()
+
+ svg.append("rect")
+ .attr("class", "background")
+ .attr("width", "100%")
+ .attr("height", "100%")
+ .call(zoom.on("zoom", redraw))
+ .on("dblclick.zoom", zoom_to_fit)
+
+ fg = svg.append("g")
+ svg_defs fg
+
+update_cola = ->
+ d3cola
+ .nodes(gdd.nodes)
+ .links(gdd.links)
+ .constraints(gdl.constraints)
+
+draw_graph = (commitish) ->
+ d3.json "deps.json/" + commitish, (error, data) ->
+ if error
+ details = JSON.parse(error.responseText)
+ gdn.error details.message
+ return
+
+ new_data = gdd.add(data)
+
+ unless new_data
+ gdn.warn "No new commits or dependencies found!"
+ update_rect_explored()
+ return
+ new_data_notification new_data
+ focus_commitish_input()
+
+ gdl.build_constraints()
+ update_cola()
+
+ paths = fg.selectAll(".link")
+ .data(gdd.links, link_key)
+ paths.enter().append("svg:path")
+ .attr("class", "link")
+ .attr("stroke", (d) -> color(link_key(d)))
+ nodes = fg.selectAll(".node")
+ .data(gdd.nodes, (d) -> d.sha1)
+ global.nodes = nodes
+
+ g_enter = nodes.enter().append("g")
+ .attr("class", "node")
+ # Questionable attempt to use dagre layout as starting positions
+ # https://github.com/tgdwyer/WebCola/issues/63
+ nodes.each (d, i) ->
+ n = gdl.node d.sha1
+ d.x = n.x
+ d.y = n.y
+ nodes.attr "transform", (d) ->
+ translate d.x, d.y
+
+ # N.B. has to be done on the update selection, i.e. *after* the enter!
+ nodes.call(d3cola.drag)
+
+ init_tip() unless tip?
+ # Event handlers need to be updated every time new nodes are added.
+ init_tip_event_handlers(nodes)
+
+ [rects, labels] = draw_new_nodes fg, g_enter
+ position_nodes(rects, labels)
+ update_rect_explored()
+
+focus_commitish_input = () ->
+ d3.select('.commitish input').node().focus()
+
+# Required for object constancy: http://bost.ocks.org/mike/constancy/ ...
+link_key = (link) ->
+ source = sha1_of_link_pointer(link.source)
+ target = sha1_of_link_pointer(link.target)
+ return source + " " + target
+
+# ... but even though link sources and targets are initially fed in
+# as indices into the nodes array, webcola then replaces the indices
+# with references to the node objects. So we have to deal with both
+# cases when ensuring we are uniquely identifying each link.
+sha1_of_link_pointer = (pointer) ->
+ return pointer.sha1 if typeof (pointer) is "object"
+ return gdd.nodes[pointer].sha1
+
+init_tip = () ->
+ tip = d3.tip().attr("class", "d3-tip").html(tip_html)
+ global.tip = tip
+ fg.call tip
+
+# A wrapper around tip.show is required to perform multiple visual
+# actions when the mouse hovers over a node; however even if the only
+# action required was to show the tool tip, the wrapper would still be
+# required in order to work around something which looks like a bug in
+# d3 or d3-tip. tip.show is defined as:
+#
+# function() {
+# var args = Array.prototype.slice.call(arguments)
+# if(args[args.length - 1] instanceof SVGElement) target = args.pop()
+# ...
+#
+# and there's also:
+#
+# function getScreenBBox() {
+# var targetel = target || d3.event.target;
+# ...
+#
+# which I'm guessing normally uses d3.event.target. However for some
+# reason when using tip.show as the dragend handler, d3.event.target
+# points to a function rather than the expected DOM element, which
+# appears to be exactly the same problem described here:
+#
+# http://stackoverflow.com/questions/12934731/d3-event-targets
+#
+# However I tried rects.call ... instead of nodes.call as suggested in
+# that SO article, but it resulted in the callback not being triggered
+# at all. By *always* providing the exact SVGElement the tip is
+# supposed to target, the desired behaviour is obtained. If
+# node_mouseover is only used in tip_dragend_handler then the target
+# gets memoised, and a normal hover-based tip.show shows the target
+# last shown by a drag, rather than the node being hovered over.
+# Weird, and annoying.
+node_mouseover = (d, i) ->
+ tip.show d, i, nodes[0][i]
+ highlight_nodes d3.select(nodes[0][i]), false
+ highlight_parents(d, i, true)
+ highlight_children(d, i, true)
+
+node_mouseout = (d, i) ->
+ tip.hide d, i, nodes[0][i]
+ highlight_nodes d3.select(nodes[0][i]), false
+ highlight_parents(d, i, false)
+ highlight_children(d, i, false)
+
+highlight_parents = (d, i, highlight) ->
+ sha1 = gdd.nodes[i].sha1
+ parents = nodes.filter (d, i) ->
+ d.sha1 of (gdd.rdeps[sha1] || {})
+ highlight_nodes parents, highlight, 'rgb(74, 200, 148)'
+
+highlight_children = (d, i, highlight) ->
+ sha1 = gdd.nodes[i].sha1
+ children = nodes.filter (d, i) ->
+ d.sha1 of (gdd.deps[sha1] || {})
+ highlight_nodes children, highlight, 'rgb(128, 197, 247)'
+
+highlight_nodes = (selection, highlight, colour='#c0c0c0') ->
+ selection.selectAll('rect')
+ .transition()
+ .ease('cubic-out')
+ .duration(200)
+ .style('stroke', if highlight then colour else '#e5e5e5')
+ .style('stroke-width', if highlight then '4px' else '2px')
+
+tip_dragend_handler = (d, i, elt) ->
+ focus_commitish_input()
+ node_mouseover d, i
+
+init_tip_event_handlers = (selection) ->
+ # We have to reuse the same drag object, otherwise only one
+ # of the event handlers will work.
+ drag = d3cola.drag()
+ hide_tip_on_drag = drag.on("drag", tip.hide)
+ on_dragend = drag.on("dragend", tip_dragend_handler)
+ selection.call hide_tip_on_drag
+ selection.call on_dragend
+
+draw_new_nodes = (fg, g_enter) ->
+ rects = g_enter.append('rect')
+ .attr('rx', 5)
+ .attr('ry', 5)
+ .on('dblclick', (d) -> launch_viewer d)
+
+ labels = g_enter.append('text').text((d) ->
+ d.name
+ ).each((d) ->
+ b = @getBBox()
+
+ # Calculate width/height of rectangle from text bounding box.
+ d.rect_width = b.width + 2 * PADDING
+ d.rect_height = b.height + 2 * PADDING
+
+ # Now set the node width/height as used by cola for
+ # positioning. This has to include the margin
+ # outside the rectangle.
+ d.width = d.rect_width + 2 * RECT_MARGIN
+ d.height = d.rect_height + 2 * RECT_MARGIN
+ )
+
+ return [rects, labels]
+
+explore_node = (d) ->
+ if d.explored
+ gdn.warn "Commit #{d.name} already explored"
+ else
+ add_commitish d.sha1
+
+launch_viewer = (d) ->
+ window.location.assign "gitfile://#{options.repo_path}##{d.sha1}"
+
+new_data_notification = (new_data) ->
+ new_nodes = new_data[0]
+ new_deps = new_data[1]
+ query = new_data[2]
+ notification =
+ if query.revspec == query.tip_sha1
+ "Analysed dependencies of #{query.revspec}"
+ else if query.revisions.length == 1
+ "<span class=\"commit-ref\">#{query.revspec}</span>
+ resolved as #{query.tip_abbrev}"
+ else
+ "<span class=\"commit-ref\">#{query.revspec}</span>
+ expanded; tip is #{query.tip_abbrev}"
+ notification += "<p>#{new_nodes} new commit"
+ notification += "s" unless new_nodes == 1
+ notification += "; #{new_deps} new " +
+ (if new_deps == 1 then "dependency" else "dependencies")
+ notification += "</p>"
+
+ gdn.success notification
+
+svg_defs = () ->
+ # define arrow markers for graph links
+ defs = svg.insert("svg:defs")
+
+ defs.append("svg:marker")
+ .attr("id", "end-arrow")
+ .attr("viewBox", "0 -5 10 10")
+ .attr("refX", 6)
+ .attr("markerWidth", 6)
+ .attr("markerHeight", 6)
+ .attr("orient", "auto")
+ .append("svg:path")
+ .attr("d", "M0,-5L10,0L0,5")
+ .attr("fill", "#000")
+
+ plus_icon = defs.append("svg:symbol")
+ .attr("id", "plus-icon")
+ .attr("viewBox", "-51 -51 102 102") # allow for stroke-width 1
+ # border
+ plus_icon.append("svg:rect")
+ .attr("width", 100)
+ .attr("height", 100)
+ .attr("fill", "#295b8c")
+ .attr("stroke", "rgb(106, 136, 200)")
+ .attr("x", -50)
+ .attr("y", -50)
+ .attr("rx", 20)
+ .attr("ry", 20)
+ # plus sign
+ plus_icon.append("svg:path")
+ .attr("d", "M-30,0 H30 M0,-30 V30")
+ .attr("stroke", "white")
+ .attr("stroke-width", 10)
+ .attr("stroke-linecap", "round")
+
+ # Uncomment to see a large version:
+ # fg.append("use")
+ # .attr("class", "plus-icon")
+ # .attr("xlink:href", "#plus-icon")
+ # .attr("width", "200")
+ # .attr("height", "200")
+ # .attr("x", 400)
+ # .attr("y", 200)
+
+position_nodes = (rects, labels) ->
+ rects
+ .attr("width", (d, i) -> d.rect_width)
+ .attr("height", (d, i) -> d.rect_height)
+ .on("mouseover", node_mouseover)
+ .on("mouseout", node_mouseout)
+
+ # Centre labels
+ labels
+ .attr("x", (d) -> d.rect_width / 2)
+ .attr("y", (d) -> d.rect_height / 2)
+ .on("mouseover", node_mouseover)
+ .on("mouseout", node_mouseout)
+
+ d3cola.start 10, 20, 20
+ d3cola.on "tick", tick_handler
+
+ # d3cola.on "end", routeEdges
+
+ # turn on overlap avoidance after first convergence
+ # d3cola.on("end", () ->
+ # unless d3cola.avoidOverlaps
+ # gdd.nodes.forEach((v) ->
+ # v.width = v.height = 10
+ # d3cola.avoidOverlaps true
+ # d3cola.start
+
+update_rect_explored = () ->
+ d3.selectAll(".node rect").attr "class", (d) ->
+ if d.explored then "explored" else "unexplored"
+ nodes.each (d) ->
+ existing_icon = d3.select(this).select("use.plus-icon")
+ if d.explored
+ existing_icon.remove()
+ else if existing_icon.empty()
+ add_plus_icon this
+
+add_plus_icon = (node_element) ->
+ n = d3.select(node_element)
+ rw = node_element.__data__.rect_width
+ rh = node_element.__data__.rect_height
+
+ icon = n.insert('use')
+ .attr('class', 'plus-icon')
+ .attr('xlink:href', '#plus-icon')
+ .attr('x', rw/2)
+ .attr('y', rh - PLUS_ICON_WIDTH/2)
+ .attr('width', 0)
+ .attr('height', 0)
+ icon
+ .on('mouseover', (d, i) -> icon_ease_in icon, rw)
+ .on('mouseout', (d, i) -> icon_ease_out icon, rw)
+ .on('click', (d) -> explore_node d)
+
+ n
+ .on('mouseover', (d, i) -> icon_ease_in icon, rw)
+ .on('mouseout', (d, i) -> icon_ease_out icon, rw)
+
+icon_ease_in = (icon, rw) ->
+ icon.transition()
+ .ease('cubic-out')
+ .duration(200)
+ .attr('width', PLUS_ICON_WIDTH)
+ .attr('height', PLUS_ICON_WIDTH)
+ .attr('x', rw/2 - PLUS_ICON_WIDTH/2)
+
+icon_ease_out = (icon, rw) ->
+ icon.transition()
+ .attr(rw/2 - PLUS_ICON_WIDTH/2)
+ .ease('cubic-out')
+ .duration(200)
+ .attr('width', 0)
+ .attr('height', 0)
+ .attr('x', rw/2)
+
+tip_html = (d) ->
+ fragment = $(tip_template).clone()
+ top = fragment.find("#fragment")
+ title = top.find("p.commit-title")
+ title.text d.title
+
+ if d.refs
+ title.append " <span />"
+ refs = title.children().first()
+ refs.addClass("commit-describe commit-ref")
+ .text(d.refs.join(" "))
+
+ top.find("span.commit-author").text(d.author_name)
+ date = new Date(d.author_time * 1000)
+ top.find("time.commit-time")
+ .attr("datetime", date.toISOString())
+ .text(date)
+ pre = top.find(".commit-body pre").text(d.body)
+
+ if options.debug
+ # deps = gdd.deps[d.sha1]
+ # if deps
+ # sha1s = [gdd.node(sha1).name for name, bool of deps]
+ # top.append("<br />Dependencies: " + sha1s.join(", "));
+ index = gdd.node_index[d.sha1]
+ debug = "<br />node index: " + index
+ dagre_node = gdl.graph.node(d.sha1)
+ debug += "<br />dagre: (#{dagre_node.x}, #{dagre_node.y})"
+ top.append debug
+
+ # Javascript *sucks*. There's no way to get the outerHTML of a
+ # document fragment, so you have to wrap the whole thing in a
+ # single parent and then look that up via children[0].
+ return fragment[0].children[0].outerHTML
+
+translate = (x, y) ->
+ "translate(#{x},#{y})"
+
+tick_handler = ->
+ nodes.each (d) ->
+ # cola sets the bounds property which is a Rectangle
+ # representing the space which other nodes should not
+ # overlap. The innerBounds property seems to tell
+ # cola the Rectangle which is the visible part of the
+ # node, minus any blank margin.
+ d.innerBounds = d.bounds.inflate(-RECT_MARGIN)
+
+ nodes.attr "transform", (d) ->
+ translate d.innerBounds.x, d.innerBounds.y
+
+ paths.each (d) ->
+ @parentNode.insertBefore this, this if isIE()
+
+ paths.attr "d", (d) ->
+ # Undocumented: https://github.com/tgdwyer/WebCola/issues/52
+ route = cola.makeEdgeBetween \
+ d.source.innerBounds,
+ d.target.innerBounds,
+ # This value is related to but not equal to the
+ # distance of arrow tip from object it points at:
+ 5
+
+ lineData = [
+ {x: route.sourceIntersection.x, y: route.sourceIntersection.y},
+ {x: route.arrowStart.x, y: route.arrowStart.y}
+ ]
+ return lineFunction lineData
+
+lineFunction = d3.svg.line()
+ .x((d) -> d.x)
+ .y((d) -> d.y)
+ .interpolate("linear")
+
+routeEdges = ->
+ d3cola.prepareEdgeRouting EDGE_ROUTING_MARGIN
+ paths.attr "d", (d) ->
+ lineFunction d3cola.routeEdge(d)
+ # show visibility graph
+ # (g) ->
+ # if d.source.id == 10 and d.target.id === 11
+ # g.E.forEach (e) =>
+ # vis.append("line").attr("x1", e.source.p.x).attr("y1", e.source.p.y)
+ # .attr("x2", e.target.p.x).attr("y2", e.target.p.y)
+ # .attr("stroke", "green")
+
+ if isIE()
+ paths.each (d) ->
+ @parentNode.insertBefore this, this
+
+isIE = ->
+ (navigator.appName is "Microsoft Internet Explorer") or
+ ((navigator.appName is "Netscape") and
+ ((new RegExp "Trident/.*rv:([0-9]{1,}[.0-9]{0,})")
+ .exec(navigator.userAgent)?))
diff --git a/git_deps/html/js/git-deps-layout.coffee b/git_deps/html/js/git-deps-layout.coffee
new file mode 100644
index 0000000..8b8cd05
--- /dev/null
+++ b/git_deps/html/js/git-deps-layout.coffee
@@ -0,0 +1,253 @@
+DEBUG = false
+
+MIN_ROW_GAP = 60
+MIN_NODE_X_GAP = 100 # presumably includes the node width
+MAX_NODE_X_GAP = 300
+MAX_NODE_Y_GAP = 80
+
+dagre = require "dagre"
+
+gdd = require "./git-deps-data.coffee"
+
+# The list of constraints to feed into WebCola.
+constraints = []
+
+# Group nodes by row, as assigned by the y coordinates returned from
+# dagre's layout(). This will map a y coordinate onto all nodes
+# within that row.
+row_groups = {}
+
+debug = (msg) ->
+ if exports.debug
+ console.log msg
+
+dagre_layout = ->
+ g = new dagre.graphlib.Graph()
+ exports.graph = g
+
+ # Set an object for the graph label
+ g.setGraph {}
+
+ # Default to assigning a new object as a label for each new edge.
+ g.setDefaultEdgeLabel -> {}
+
+ for node in gdd.nodes
+ g.setNode node.sha1,
+ label: node.name
+ width: node.rect_width or 70
+ height: node.rect_height or 30
+
+ for parent_sha1, children of gdd.deps
+ for child_sha1, bool of children
+ g.setEdge parent_sha1, child_sha1
+
+ dagre.layout g
+ return g
+
+dagre_row_groups = ->
+ g = dagre_layout()
+ row_groups = {}
+ exports.row_groups = row_groups
+ for sha1 in g.nodes()
+ x = g.node(sha1).x
+ y = g.node(sha1).y
+ row_groups[y] = [] unless y of row_groups
+ row_groups[y].push
+ sha1: sha1
+ x: x
+
+ for y, nodes of row_groups
+ nodes.sort (n) -> -n.x
+
+ return row_groups
+
+build_constraints = ->
+ row_groups = dagre_row_groups()
+ debug "build_constraints"
+ for y, row_nodes of row_groups
+ debug y
+ debug row_nodes
+
+ constraints.length = 0 # FIXME: only rebuild constraints which changed
+
+ # We want alignment constraints between all nodes which dagre
+ # assigned the same y value.
+ #row_alignment_constraints(row_groups)
+
+ # We need separation constraints ensuring that the left-to-right
+ # ordering within each row assigned by dagre is preserved.
+ for y, row_nodes of row_groups
+ # No point having an alignment group with only one node in.
+ continue if row_nodes.length <= 1
+
+ # Multiple constraints per row.
+ debug "ordering for row y=#{y}"
+ row_node_ordering_constraints(row_nodes)
+ debug_constraints()
+
+ # We need separation constraints ensuring that the top-to-bottom
+ # ordering assigned by dagre is preserved. Since all nodes within
+ # a single row are already constrained to the same y coordinate
+ # from above, one would have hoped it would be enough to only have
+ # separation between a single node in adjacent rows:
+ #
+ # row_ordering_constraints(row_groups)
+
+ # However, due to https://github.com/tgdwyer/WebCola/issues/61
+ # there is more flexibility for y-coordinates within a row than we
+ # want, so instead we order rows using dependencies.
+ dependency_ordering_constraints()
+
+debug_constraints = (cs = constraints) ->
+ for c in cs
+ debug c
+ return
+
+row_alignment_constraints = (row_groups) ->
+ row_alignment_constraint(row_nodes) \
+ for y, row_nodes of row_groups when row_nodes.length > 1
+
+row_alignment_constraint = (row_nodes) ->
+ debug 'row_alignment_constraint'
+ # A standard alignment constraint (one per row) is too strict
+ # because it doesn't give cola enough "wiggle room":
+ #
+ # constraint =
+ # axis: "y"
+ # type: "alignment"
+ # offsets: []
+ #
+ # for node in row_nodes
+ # constraint.offsets.push
+ # node: gdd.node_index[node.sha1],
+ # offset: 0
+ #
+ # constraints.push constraint
+ #
+ # So instead we use vertical min/max separation constraints:
+ i = 0
+ while i < row_nodes.length - 1
+ left = row_nodes[i]
+ right = row_nodes[i+1]
+ mm = max_unordered_separation_constraints \
+ 'y', MAX_NODE_Y_GAP,
+ gdd.node_index[left.sha1],
+ gdd.node_index[right.sha1]
+ exports.constraints = constraints = constraints.concat mm
+ i++
+ debug_constraints()
+ return
+
+row_node_ordering_constraints = (row_nodes) ->
+ debug 'row_node_ordering_constraints'
+ i = 0
+ while i < row_nodes.length - 1
+ left = row_nodes[i]
+ right = row_nodes[i+1]
+ left_i = gdd.node_index[left.sha1]
+ right_i = gdd.node_index[right.sha1]
+ debug " #{left_i} < #{right_i} (#{left.x} < #{right.x})"
+ # mm = min_max_ordered_separation_constraints \
+ # 'x', MIN_NODE_X_GAP, MAX_NODE_X_GAP, left_i, right_i
+ min = min_separation_constraint \
+ 'x', MIN_NODE_X_GAP, left_i, right_i
+ exports.constraints = constraints = constraints.concat min
+ i++
+ return
+
+row_ordering_constraints = (row_groups) ->
+ debug 'row_ordering_constraints'
+ row_y_coords = Object.keys(row_groups).sort()
+
+ i = 0
+ while i < row_y_coords.length - 1
+ upper_y = row_y_coords[i]
+ lower_y = row_y_coords[i + 1]
+ upper_node = row_groups[upper_y][0]
+ lower_node = row_groups[lower_y][0]
+ constraints.push \
+ min_separation_constraint \
+ 'y', MIN_ROW_GAP,
+ gdd.node_index[upper_node.sha1],
+ gdd.node_index[lower_node.sha1]
+
+ i++
+ debug_constraints()
+ return
+
+dependency_ordering_constraints = () ->
+ debug 'dependency_ordering_constraints'
+
+ for parent_sha1, children of gdd.deps
+ child_sha1s = Object.keys(children).sort (sha1) -> node(sha1).x
+ dependency_ordering_constraint(parent_sha1, child_sha1s[0])
+ len = child_sha1s.length
+ if len > 1
+ dependency_ordering_constraint(parent_sha1, child_sha1s[len-1])
+ if len > 2
+ middle = Math.floor(len / 2)
+ dependency_ordering_constraint(parent_sha1, child_sha1s[middle])
+
+ debug_constraints()
+ return
+
+dependency_ordering_constraint = (parent_sha1, child_sha1) ->
+ constraints.push \
+ min_separation_constraint \
+ 'y', MIN_ROW_GAP,
+ gdd.node_index[parent_sha1],
+ gdd.node_index[child_sha1]
+
+##################################################################
+# helpers
+
+# Uses approach explained here:
+# https://github.com/tgdwyer/WebCola/issues/62#issuecomment-69571870
+min_max_ordered_separation_constraints = (axis, min, max, left, right) ->
+ return [
+ min_separation_constraint(axis, min, left, right),
+ max_separation_constraint(axis, max, left, right)
+ ]
+
+# https://github.com/tgdwyer/WebCola/issues/66
+max_unordered_separation_constraints = (axis, max, left, right) ->
+ return [
+ max_separation_constraint(axis, max, left, right),
+ max_separation_constraint(axis, max, right, left)
+ ]
+
+min_separation_constraint = (axis, gap, left, right) ->
+ {} =
+ axis: axis
+ gap: gap
+ left: left
+ right: right
+
+# We use a negative gap and reverse the inequality, in order to
+# achieve a maximum rather than minimum separation gap. However this
+# does not prevent the nodes from overlapping or even swapping order.
+# For that you also need a min_separation_constraint, but it's more
+# convenient to use min_max_ordered_separation_constraints. See
+# https://github.com/tgdwyer/WebCola/issues/62#issuecomment-69571870
+# for more details.
+max_separation_constraint = (axis, gap, left, right) ->
+ {} =
+ axis: axis
+ gap: -gap
+ left: right
+ right: left
+
+node = (sha1) ->
+ exports.graph.node sha1
+
+module.exports = exports =
+ # Variables have to be exported every time they're assigned,
+ # since assignment creates a new object and associated reference
+
+ # Functions
+ build_constraints: build_constraints
+ debug_constraints: debug_constraints
+ node: node
+
+ # Variables
+ debug: DEBUG
diff --git a/git_deps/html/js/git-deps-noty.coffee b/git_deps/html/js/git-deps-noty.coffee
new file mode 100644
index 0000000..cec2b08
--- /dev/null
+++ b/git_deps/html/js/git-deps-noty.coffee
@@ -0,0 +1,32 @@
+noty = require "noty"
+
+# Different noty types:
+# alert, success, error, warning, information, confirmation
+noty_error = (text) -> notyfication "error", text
+noty_warn = (text) -> notyfication "warning", text
+noty_success = (text) -> notyfication "success", text
+noty_info = (text) -> notyfication "information", text
+noty_debug = (text) -> notyfication "information", text
+
+# "notyfication" - haha, did you see what I did there?
+notyfication = (type, text) ->
+ noty(
+ text: text
+ type: type
+ layout: "topRight"
+ theme: "relax"
+ maxVisible: 15
+ timeout: 30000 # ms
+ animation:
+ open: "animated bounceInUp" # Animate.css class names
+ close: "animated bounceOutUp" # Animate.css class names
+ easing: "swing" # unavailable - no need
+ speed: 500 # unavailable - no need
+ )
+
+module.exports =
+ error: noty_error
+ warn: noty_warn
+ success: noty_success
+ info: noty_info
+ debug: noty_debug
diff --git a/git_deps/html/package.json b/git_deps/html/package.json
new file mode 100644
index 0000000..d4dfcd7
--- /dev/null
+++ b/git_deps/html/package.json
@@ -0,0 +1,54 @@
+{
+ "name": "git-deps",
+ "version": "0.1.0",
+ "authors": [
+ "Adam Spiers"
+ ],
+ "description": "tool for performing automatic analysis of dependencies between git commits",
+ "main": "git-deps",
+ "keywords": [
+ "git",
+ "dependency",
+ "analysis",
+ "scm",
+ "graphing",
+ "visualization"
+ ],
+ "license": "GPL-2.0",
+ "homepage": "https://github.com/aspiers/git-deps",
+ "private": true,
+ "ignore": [
+ "**/.*",
+ "node_modules",
+ "bower_components",
+ "test",
+ "tests"
+ ],
+ "dependencies": {
+ "d3-tip": "~0.6.6",
+ "d3": "~3.5.3",
+ "webcola": "aspiers/WebCola#git-deps-master",
+ "jquery": "~2.1.3",
+ "noty": "~v2.4.1",
+ "browserify": "*",
+ "coffeeify" : "~1.0.0",
+ "dagre": "~0.7.1"
+ },
+ "devDependencies": {
+ "watchify": "*"
+ },
+ "directories": {
+ "test": "test"
+ },
+ "scripts": {
+ "test": "echo \"Error: no test specified\" && exit 1"
+ },
+ "repository": {
+ "type": "git",
+ "url": "https://github.com/aspiers/git-deps"
+ },
+ "author": "Adam Spiers",
+ "bugs": {
+ "url": "https://github.com/aspiers/git-deps/issues"
+ }
+}
diff --git a/git_deps/html/test.json b/git_deps/html/test.json
new file mode 100644
index 0000000..3d73289
--- /dev/null
+++ b/git_deps/html/test.json
@@ -0,0 +1,442 @@
+{
+ "commits": [
+ {
+ "author_mail": "git@adamspiers.org",
+ "author_name": "Adam Spiers",
+ "author_offset": 0,
+ "author_time": 1420486941,
+ "body": "This creates the JSON which will eventually be consumed by\nthe Javascript visualizer.\n",
+ "committer_mail": "git@adamspiers.org",
+ "committer_name": "Adam Spiers",
+ "committer_offset": 0,
+ "committer_time": 1420487137,
+ "describe": "tags/test-0",
+ "name": "2b6d591",
+ "separator": "\n",
+ "sha": "2b6d5915f6433b9eb1685751b82cfbebcbb37981",
+ "title": "add JSON listener"
+ },
+ {
+ "author_mail": "git@adamspiers.org",
+ "author_name": "Adam Spiers",
+ "author_offset": -300,
+ "author_time": 1384447153,
+ "body": "Automatic git commit dependency inference tool.\n\nOriginally committed to:\n\n https://github.com/aspiers/git-config/blob/master/bin/git-deps\n\nand then split off into this repository via git filter-branch\nand other hackery, preserving history.\n",
+ "committer_mail": "git@adamspiers.org",
+ "committer_name": "Adam Spiers",
+ "committer_offset": 0,
+ "committer_time": 1420476874,
+ "describe": "",
+ "name": "b196757",
+ "separator": "\n",
+ "sha": "b1967573e81a8100a4cc778936de0ba0a8a8f5cb",
+ "title": "first prototype of git-deps"
+ },
+ {
+ "author_mail": "git@adamspiers.org",
+ "author_name": "Adam Spiers",
+ "author_offset": -300,
+ "author_time": 1384471712,
+ "body": "",
+ "committer_mail": "git@adamspiers.org",
+ "committer_name": "Adam Spiers",
+ "committer_offset": 0,
+ "committer_time": 1420477027,
+ "describe": "",
+ "name": "3a1dd42",
+ "separator": "\n",
+ "sha": "3a1dd42fd6114a634ba7cf037ce61e2aee76db73",
+ "title": "add logging and recursion"
+ },
+ {
+ "author_mail": "git@adamspiers.org",
+ "author_name": "Adam Spiers",
+ "author_offset": -300,
+ "author_time": 1384563102,
+ "body": "",
+ "committer_mail": "git@adamspiers.org",
+ "committer_name": "Adam Spiers",
+ "committer_offset": 0,
+ "committer_time": 1420477027,
+ "describe": "",
+ "name": "3374b84",
+ "separator": "\n",
+ "sha": "3374b8419a45d91d3c0631be11c8cf893b272217",
+ "title": "add listener classes"
+ },
+ {
+ "author_mail": "git@adamspiers.org",
+ "author_name": "Adam Spiers",
+ "author_offset": 0,
+ "author_time": 1420483579,
+ "body": "",
+ "committer_mail": "git@adamspiers.org",
+ "committer_name": "Adam Spiers",
+ "committer_offset": 0,
+ "committer_time": 1420483579,
+ "describe": "",
+ "name": "ff82dda",
+ "separator": "\n",
+ "sha": "ff82dda196947650bd497301e61b282753193564",
+ "title": "fix a bunch of PEP8 issues"
+ },
+ {
+ "author_mail": "git@adamspiers.org",
+ "author_name": "Adam Spiers",
+ "author_offset": -300,
+ "author_time": 1384579026,
+ "body": "",
+ "committer_mail": "git@adamspiers.org",
+ "committer_name": "Adam Spiers",
+ "committer_offset": 0,
+ "committer_time": 1420477028,
+ "describe": "",
+ "name": "8d44254",
+ "separator": "\n",
+ "sha": "8d442544a20b706b996d66ab390a16fd97b48d6d",
+ "title": "use new-style classes"
+ },
+ {
+ "author_mail": "git@adamspiers.org",
+ "author_name": "Adam Spiers",
+ "author_offset": -300,
+ "author_time": 1384452615,
+ "body": "",
+ "committer_mail": "git@adamspiers.org",
+ "committer_name": "Adam Spiers",
+ "committer_offset": 0,
+ "committer_time": 1420477027,
+ "describe": "",
+ "name": "b144bfd",
+ "separator": "\n",
+ "sha": "b144bfd5feb327ef7ce0c26bbfb6f4da573abfe5",
+ "title": "refactor into new DependencyDetector class"
+ },
+ {
+ "author_mail": "git@adamspiers.org",
+ "author_name": "Adam Spiers",
+ "author_offset": 0,
+ "author_time": 1420482533,
+ "body": "",
+ "committer_mail": "git@adamspiers.org",
+ "committer_name": "Adam Spiers",
+ "committer_offset": 0,
+ "committer_time": 1420482533,
+ "describe": "",
+ "name": "e406002",
+ "separator": "\n",
+ "sha": "e40600230d1c3059485437bd4d5690d61c9edb2f",
+ "title": "don't show \"[False]\" default for boolean options"
+ },
+ {
+ "author_mail": "git@adamspiers.org",
+ "author_name": "Adam Spiers",
+ "author_offset": -300,
+ "author_time": 1384650040,
+ "body": "",
+ "committer_mail": "git@adamspiers.org",
+ "committer_name": "Adam Spiers",
+ "committer_offset": 0,
+ "committer_time": 1420477028,
+ "describe": "",
+ "name": "4f27a1e",
+ "separator": "\n",
+ "sha": "4f27a1ee2b5fd63a58311a20e2aed0a24eda8da2",
+ "title": "add --exclude-commits"
+ },
+ {
+ "author_mail": "git@adamspiers.org",
+ "author_name": "Adam Spiers",
+ "author_offset": -300,
+ "author_time": 1384656388,
+ "body": "",
+ "committer_mail": "git@adamspiers.org",
+ "committer_name": "Adam Spiers",
+ "committer_offset": 0,
+ "committer_time": 1420477028,
+ "describe": "",
+ "name": "824f84c",
+ "separator": "\n",
+ "sha": "824f84cd594254d0c87f330b855153fc5ffe5ad3",
+ "title": "add installation instructions"
+ },
+ {
+ "author_mail": "git@adamspiers.org",
+ "author_name": "Adam Spiers",
+ "author_offset": -300,
+ "author_time": 1384579202,
+ "body": "",
+ "committer_mail": "git@adamspiers.org",
+ "committer_name": "Adam Spiers",
+ "committer_offset": 0,
+ "committer_time": 1420477028,
+ "describe": "",
+ "name": "6240939",
+ "separator": "\n",
+ "sha": "62409395e260ad01f9ae7b84869f5516ef80c7aa",
+ "title": "output dependencies as soon as they're found"
+ },
+ {
+ "author_mail": "git@adamspiers.org",
+ "author_name": "Adam Spiers",
+ "author_offset": -300,
+ "author_time": 1384654780,
+ "body": "",
+ "committer_mail": "git@adamspiers.org",
+ "committer_name": "Adam Spiers",
+ "committer_offset": 0,
+ "committer_time": 1420477028,
+ "describe": "",
+ "name": "5071249",
+ "separator": "\n",
+ "sha": "5071249715e82dcf3c1db12eec28c1232aba2142",
+ "title": "avoid adding entries to TODO queue multiple times"
+ },
+ {
+ "author_mail": "git@adamspiers.org",
+ "author_name": "Adam Spiers",
+ "author_offset": -300,
+ "author_time": 1384566591,
+ "body": "",
+ "committer_mail": "git@adamspiers.org",
+ "committer_name": "Adam Spiers",
+ "committer_offset": 0,
+ "committer_time": 1420477027,
+ "describe": "",
+ "name": "6e86e8b",
+ "separator": "\n",
+ "sha": "6e86e8b7f648bd6a3a6d3216aa5899414b65cbed",
+ "title": "don't crash on commits which only add files"
+ },
+ {
+ "author_mail": "git@adamspiers.org",
+ "author_name": "Adam Spiers",
+ "author_offset": -300,
+ "author_time": 1384563081,
+ "body": "",
+ "committer_mail": "git@adamspiers.org",
+ "committer_name": "Adam Spiers",
+ "committer_offset": 0,
+ "committer_time": 1420477027,
+ "describe": "",
+ "name": "acc24a4",
+ "separator": "\n",
+ "sha": "acc24a404d82061bbc6db5afb146d83bf131830b",
+ "title": "add --context-lines"
+ },
+ {
+ "author_mail": "git@adamspiers.org",
+ "author_name": "Adam Spiers",
+ "author_offset": -300,
+ "author_time": 1384567428,
+ "body": "",
+ "committer_mail": "git@adamspiers.org",
+ "committer_name": "Adam Spiers",
+ "committer_offset": 0,
+ "committer_time": 1420477028,
+ "describe": "",
+ "name": "5ec5ccb",
+ "separator": "\n",
+ "sha": "5ec5ccbdff508014c61ae9d18f3366a15c0f2689",
+ "title": "add first line of commits to debug"
+ },
+ {
+ "author_mail": "git@adamspiers.org",
+ "author_name": "Adam Spiers",
+ "author_offset": -300,
+ "author_time": 1384563342,
+ "body": "",
+ "committer_mail": "git@adamspiers.org",
+ "committer_name": "Adam Spiers",
+ "committer_offset": 0,
+ "committer_time": 1420477027,
+ "describe": "",
+ "name": "f2cddb4",
+ "separator": "\n",
+ "sha": "f2cddb4aa00de4ddff2cdca251758e25e95e04ad",
+ "title": "tweaks to improve debugging"
+ },
+ {
+ "author_mail": "git@adamspiers.org",
+ "author_name": "Adam Spiers",
+ "author_offset": -300,
+ "author_time": 1384651799,
+ "body": "",
+ "committer_mail": "git@adamspiers.org",
+ "committer_name": "Adam Spiers",
+ "committer_offset": 0,
+ "committer_time": 1420477028,
+ "describe": "",
+ "name": "1b66efa",
+ "separator": "\n",
+ "sha": "1b66efa173a19a8b4c0c47274a1b9cdd8b9912af",
+ "title": "improve help text"
+ },
+ {
+ "author_mail": "git@adamspiers.org",
+ "author_name": "Adam Spiers",
+ "author_offset": -300,
+ "author_time": 1384567528,
+ "body": "",
+ "committer_mail": "git@adamspiers.org",
+ "committer_name": "Adam Spiers",
+ "committer_offset": 0,
+ "committer_time": 1420477028,
+ "describe": "",
+ "name": "80c247f",
+ "separator": "\n",
+ "sha": "80c247fd21a1e7f476d1c8ba289498e216eff3dc",
+ "title": "--help: put short options first"
+ },
+ {
+ "author_mail": "git@adamspiers.org",
+ "author_name": "Adam Spiers",
+ "author_offset": -300,
+ "author_time": 1384567489,
+ "body": "",
+ "committer_mail": "git@adamspiers.org",
+ "committer_name": "Adam Spiers",
+ "committer_offset": 0,
+ "committer_time": 1420477028,
+ "describe": "",
+ "name": "2ebcb2b",
+ "separator": "\n",
+ "sha": "2ebcb2b6081e32e9a463519525bd432287b24520",
+ "title": "improve --help for --context-lines"
+ },
+ {
+ "author_mail": "git@adamspiers.org",
+ "author_name": "Adam Spiers",
+ "author_offset": -300,
+ "author_time": 1384636660,
+ "body": "",
+ "committer_mail": "git@adamspiers.org",
+ "committer_name": "Adam Spiers",
+ "committer_offset": 0,
+ "committer_time": 1420477028,
+ "describe": "",
+ "name": "f7bf058",
+ "separator": "\n",
+ "sha": "f7bf058439fd7499aad7a10418a9f516e6949fbc",
+ "title": "allow multiple dependents on ARGV, and fix usage string"
+ },
+ {
+ "author_mail": "git@adamspiers.org",
+ "author_name": "Adam Spiers",
+ "author_offset": -300,
+ "author_time": 1384612401,
+ "body": "",
+ "committer_mail": "git@adamspiers.org",
+ "committer_name": "Adam Spiers",
+ "committer_offset": 0,
+ "committer_time": 1420477028,
+ "describe": "",
+ "name": "2a05400",
+ "separator": "\n",
+ "sha": "2a05400e232e14f0d4c1cbfb548a0871ea57bd44",
+ "title": "ignore KeyboardInterrupt"
+ },
+ {
+ "author_mail": "git@adamspiers.org",
+ "author_name": "Adam Spiers",
+ "author_offset": -300,
+ "author_time": 1384652004,
+ "body": "",
+ "committer_mail": "git@adamspiers.org",
+ "committer_name": "Adam Spiers",
+ "committer_offset": 0,
+ "committer_time": 1420477028,
+ "describe": "",
+ "name": "4364944",
+ "separator": "\n",
+ "sha": "43649442f49876ad22051b085a9258f39bbcd5c6",
+ "title": "fix error message"
+ }
+ ],
+ "dependencies": [
+ {
+ "child": "b1967573e81a8100a4cc778936de0ba0a8a8f5cb",
+ "parent": "2b6d5915f6433b9eb1685751b82cfbebcbb37981"
+ },
+ {
+ "child": "3a1dd42fd6114a634ba7cf037ce61e2aee76db73",
+ "parent": "2b6d5915f6433b9eb1685751b82cfbebcbb37981"
+ },
+ {
+ "child": "3374b8419a45d91d3c0631be11c8cf893b272217",
+ "parent": "2b6d5915f6433b9eb1685751b82cfbebcbb37981"
+ },
+ {
+ "child": "ff82dda196947650bd497301e61b282753193564",
+ "parent": "2b6d5915f6433b9eb1685751b82cfbebcbb37981"
+ },
+ {
+ "child": "8d442544a20b706b996d66ab390a16fd97b48d6d",
+ "parent": "2b6d5915f6433b9eb1685751b82cfbebcbb37981"
+ },
+ {
+ "child": "b144bfd5feb327ef7ce0c26bbfb6f4da573abfe5",
+ "parent": "2b6d5915f6433b9eb1685751b82cfbebcbb37981"
+ },
+ {
+ "child": "e40600230d1c3059485437bd4d5690d61c9edb2f",
+ "parent": "2b6d5915f6433b9eb1685751b82cfbebcbb37981"
+ },
+ {
+ "child": "4f27a1ee2b5fd63a58311a20e2aed0a24eda8da2",
+ "parent": "2b6d5915f6433b9eb1685751b82cfbebcbb37981"
+ },
+ {
+ "child": "824f84cd594254d0c87f330b855153fc5ffe5ad3",
+ "parent": "ff82dda196947650bd497301e61b282753193564"
+ },
+ {
+ "child": "62409395e260ad01f9ae7b84869f5516ef80c7aa",
+ "parent": "ff82dda196947650bd497301e61b282753193564"
+ },
+ {
+ "child": "5071249715e82dcf3c1db12eec28c1232aba2142",
+ "parent": "ff82dda196947650bd497301e61b282753193564"
+ },
+ {
+ "child": "6e86e8b7f648bd6a3a6d3216aa5899414b65cbed",
+ "parent": "ff82dda196947650bd497301e61b282753193564"
+ },
+ {
+ "child": "acc24a404d82061bbc6db5afb146d83bf131830b",
+ "parent": "ff82dda196947650bd497301e61b282753193564"
+ },
+ {
+ "child": "5ec5ccbdff508014c61ae9d18f3366a15c0f2689",
+ "parent": "ff82dda196947650bd497301e61b282753193564"
+ },
+ {
+ "child": "f2cddb4aa00de4ddff2cdca251758e25e95e04ad",
+ "parent": "ff82dda196947650bd497301e61b282753193564"
+ },
+ {
+ "child": "1b66efa173a19a8b4c0c47274a1b9cdd8b9912af",
+ "parent": "ff82dda196947650bd497301e61b282753193564"
+ },
+ {
+ "child": "80c247fd21a1e7f476d1c8ba289498e216eff3dc",
+ "parent": "ff82dda196947650bd497301e61b282753193564"
+ },
+ {
+ "child": "2ebcb2b6081e32e9a463519525bd432287b24520",
+ "parent": "ff82dda196947650bd497301e61b282753193564"
+ },
+ {
+ "child": "f7bf058439fd7499aad7a10418a9f516e6949fbc",
+ "parent": "ff82dda196947650bd497301e61b282753193564"
+ },
+ {
+ "child": "2a05400e232e14f0d4c1cbfb548a0871ea57bd44",
+ "parent": "ff82dda196947650bd497301e61b282753193564"
+ },
+ {
+ "child": "43649442f49876ad22051b085a9258f39bbcd5c6",
+ "parent": "5071249715e82dcf3c1db12eec28c1232aba2142"
+ }
+ ]
+}
diff --git a/git_deps/html/tip-template.html b/git_deps/html/tip-template.html
new file mode 100644
index 0000000..1362574
--- /dev/null
+++ b/git_deps/html/tip-template.html
@@ -0,0 +1,11 @@
+<div id="fragment">
+ <p class="commit-title" />
+ <div class="commit-meta">
+ <span class="commit-author"></span>
+ authored on
+ <time class="commit-time" is="relative-time" />
+ </div>
+ <div class="commit-body">
+ <pre />
+ </div>
+</div>
diff --git a/git_deps/listener/__init__.py b/git_deps/listener/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/git_deps/listener/__init__.py
diff --git a/git_deps/listener/base.py b/git_deps/listener/base.py
new file mode 100644
index 0000000..594f438
--- /dev/null
+++ b/git_deps/listener/base.py
@@ -0,0 +1,35 @@
+class DependencyListener(object):
+ """Class for listening to result events generated by
+ DependencyDetector. Add an instance of this class to a
+ DependencyDetector instance via DependencyDetector.add_listener().
+ """
+
+ def __init__(self, options):
+ self.options = options
+
+ def set_detector(self, detector):
+ self.detector = detector
+
+ def repo(self):
+ return self.detector.repo
+
+ def new_commit(self, commit):
+ pass
+
+ def new_dependent(self, dependent):
+ pass
+
+ def new_dependency(self, dependent, dependency, path, line_num):
+ pass
+
+ def new_path(self, dependent, dependency, path, line_num):
+ pass
+
+ def new_line(self, dependent, dependency, path, line_num):
+ pass
+
+ def dependent_done(self, dependent, dependencies):
+ pass
+
+ def all_done(self):
+ pass
diff --git a/git_deps/listener/cli.py b/git_deps/listener/cli.py
new file mode 100644
index 0000000..285c622
--- /dev/null
+++ b/git_deps/listener/cli.py
@@ -0,0 +1,56 @@
+import subprocess
+
+from git_deps.listener.base import DependencyListener
+
+
+class CLIDependencyListener(DependencyListener):
+ """Dependency listener for use when running in CLI mode.
+
+ This allows us to output dependencies as they are discovered,
+ rather than waiting for all dependencies to be discovered before
+ outputting anything; the latter approach can make the user wait
+ too long for useful output if recursion is enabled.
+ """
+
+ def __init__(self, options):
+ super(CLIDependencyListener, self).__init__(options)
+
+ # Count each mention of each revision, so we can avoid duplicating
+ # commits in the output.
+ self._revs = {}
+
+ def new_commit(self, commit):
+ rev = commit.hex
+ if rev not in self._revs:
+ self._revs[rev] = 0
+ self._revs[rev] += 1
+
+ def new_dependency(self, dependent, dependency, path, line_num):
+ dependent_sha1 = dependent.hex
+ dependency_sha1 = dependency.hex
+
+ if self.options.multi:
+ if self.options.log:
+ print("%s depends on:" % dependent_sha1)
+ else:
+ print("%s %s" % (dependent_sha1, dependency_sha1))
+ else:
+ if not self.options.log and self._revs[dependency_sha1] <= 1:
+ print(dependency_sha1)
+
+ if self.options.log and self._revs[dependency_sha1] <= 1:
+ cmd = [
+ 'git',
+ '--no-pager',
+ '-c', 'color.ui=always',
+ 'log', '-n1',
+ dependency_sha1
+ ]
+ print(subprocess.check_output(cmd))
+ # dependency = detector.get_commit(dependency_sha1)
+ # print(dependency.message + "\n")
+
+ # for path in self.dependencies[dependency]:
+ # print(" %s" % path)
+ # keys = sorted(self.dependencies[dependency][path].keys()
+ # print(" %s" % ", ".join(keys)))
diff --git a/git_deps/listener/json.py b/git_deps/listener/json.py
new file mode 100644
index 0000000..aedb6fa
--- /dev/null
+++ b/git_deps/listener/json.py
@@ -0,0 +1,86 @@
+from git_deps.listener.base import DependencyListener
+
+from git_deps.gitutils import GitUtils
+
+
+class JSONDependencyListener(DependencyListener):
+ """Dependency listener for use when compiling graph data in a JSON
+ format which can be consumed by WebCola / d3. Each new commit has
+ to be added to a 'commits' array.
+ """
+
+ def __init__(self, options):
+ super(JSONDependencyListener, self).__init__(options)
+
+ # Map commit names to indices in the commits array. This is used
+ # to avoid the risk of duplicates in the commits array, which
+ # could happen when recursing, since multiple commits could
+ # potentially depend on the same commit.
+ self._commits = {}
+
+ self._json = {
+ 'commits': [],
+ 'dependencies': [],
+ }
+
+ def get_commit(self, sha1):
+ i = self._commits[sha1]
+ return self._json['commits'][i]
+
+ def add_commit(self, commit):
+ """Adds the commit to the commits array if it doesn't already exist,
+ and returns the commit's index in the array.
+ """
+ sha1 = commit.hex
+ if sha1 in self._commits:
+ return self._commits[sha1]
+ title, separator, body = commit.message.partition("\n")
+ commit = {
+ 'explored': False,
+ 'sha1': sha1,
+ 'name': GitUtils.abbreviate_sha1(sha1),
+ 'describe': GitUtils.describe(sha1),
+ 'refs': GitUtils.refs_to(sha1, self.repo()),
+ 'author_name': commit.author.name,
+ 'author_mail': commit.author.email,
+ 'author_time': commit.author.time,
+ 'author_offset': commit.author.offset,
+ 'committer_name': commit.committer.name,
+ 'committer_mail': commit.committer.email,
+ 'committer_time': commit.committer.time,
+ 'committer_offset': commit.committer.offset,
+ # 'message': commit.message,
+ 'title': title,
+ 'separator': separator,
+ 'body': body.lstrip("\n"),
+ }
+ self._json['commits'].append(commit)
+ self._commits[sha1] = len(self._json['commits']) - 1
+ return self._commits[sha1]
+
+ def add_link(self, source, target):
+ self._json['dependencies'].append
+
+ def new_commit(self, commit):
+ self.add_commit(commit)
+
+ def new_dependency(self, parent, child, path, line_num):
+ ph = parent.hex
+ ch = child.hex
+
+ new_dep = {
+ 'parent': ph,
+ 'child': ch,
+ }
+
+ if self.options.log:
+ pass # FIXME
+
+ self._json['dependencies'].append(new_dep)
+
+ def dependent_done(self, dependent, dependencies):
+ commit = self.get_commit(dependent.hex)
+ commit['explored'] = True
+
+ def json(self):
+ return self._json
diff --git a/git_deps/server.py b/git_deps/server.py
new file mode 100644
index 0000000..e996a33
--- /dev/null
+++ b/git_deps/server.py
@@ -0,0 +1,122 @@
+import os
+import subprocess
+
+from gitutils import GitUtils
+from git_deps.detector import DependencyDetector
+from git_deps.errors import InvalidCommitish
+from git_deps.listener.json import JSONDependencyListener
+from git_deps.utils import abort
+
+
+def serve(options):
+ try:
+ import flask
+ from flask import Flask, send_file, safe_join
+ from flask.json import jsonify
+ except ImportError:
+ abort("Cannot find flask module which is required for webserver mode.")
+
+ webserver = Flask('git-deps')
+ here = os.path.dirname(os.path.realpath(__file__))
+ root = os.path.join(here, 'html')
+ webserver.root_path = root
+
+ ##########################################################
+ # Static content
+
+ @webserver.route('/')
+ def main_page():
+ return send_file('git-deps.html')
+
+ @webserver.route('/tip-template.html')
+ def tip_template():
+ return send_file('tip-template.html')
+
+ @webserver.route('/test.json')
+ def data():
+ return send_file('test.json')
+
+ def make_subdir_handler(subdir):
+ def subdir_handler(filename):
+ path = safe_join(root, subdir)
+ path = safe_join(path, filename)
+ if os.path.exists(path):
+ return send_file(path)
+ else:
+ flask.abort(404)
+ return subdir_handler
+
+ for subdir in ('node_modules', 'css', 'js'):
+ fn = make_subdir_handler(subdir)
+ route = '/%s/<path:filename>' % subdir
+ webserver.add_url_rule(route, subdir + '_handler', fn)
+
+ ##########################################################
+ # Dynamic content
+
+ def json_error(status_code, error_class, message, **extra):
+ json = {
+ 'status': status_code,
+ 'error_class': error_class,
+ 'message': message,
+ }
+ json.update(extra)
+ response = jsonify(json)
+ response.status_code = status_code
+ return response
+
+ @webserver.route('/options')
+ def send_options():
+ client_options = options.__dict__
+ client_options['repo_path'] = os.getcwd()
+ return jsonify(client_options)
+
+ @webserver.route('/deps.json/<revspec>')
+ def deps(revspec):
+ detector = DependencyDetector(options)
+ listener = JSONDependencyListener(options)
+ detector.add_listener(listener)
+
+ if '..' in revspec:
+ try:
+ revisions = GitUtils.rev_list(revspec)
+ except subprocess.CalledProcessError as e:
+ return json_err(
+ 422, 'Invalid revision range',
+ "Could not resolve revision range '%s'" % revspec,
+ revspec=revspec)
+ else:
+ revisions = [revspec]
+
+ for rev in revisions:
+ try:
+ commit = detector.get_commit(rev)
+ except InvalidCommitish as e:
+ return json_error(
+ 422, 'Invalid revision',
+ "Could not resolve revision '%s'" % rev,
+ rev=rev)
+
+ detector.find_dependencies(rev)
+
+ tip_commit = detector.get_commit(revisions[0])
+ tip_sha1 = tip_commit.hex
+
+ json = listener.json()
+ json['query'] = {
+ 'revspec': revspec,
+ 'revisions': revisions,
+ 'tip_sha1': tip_sha1,
+ 'tip_abbrev': GitUtils.abbreviate_sha1(tip_sha1),
+ }
+ return jsonify(json)
+
+ # We don't want to see double-decker warnings, so check
+ # WERKZEUG_RUN_MAIN which is only set for the first startup, not
+ # on app reloads.
+ if options.debug and not os.getenv('WERKZEUG_RUN_MAIN'):
+ print("!! WARNING! Debug mode enabled, so webserver is completely "
+ "insecure!")
+ print("!! Arbitrary code can be executed from browser!")
+ print()
+ webserver.run(port=options.port, debug=options.debug, host=options.bindaddr)
diff --git a/git_deps/utils.py b/git_deps/utils.py
new file mode 100644
index 0000000..3661c00
--- /dev/null
+++ b/git_deps/utils.py
@@ -0,0 +1,8 @@
+from __future__ import print_function
+
+import sys
+
+
+def abort(msg, exitcode=1):
+ print(msg, file=sys.stderr)
+ sys.exit(exitcode)