From 0e6ce21361c5d8e35cd15327539eec1f627aa0e3 Mon Sep 17 00:00:00 2001 From: Mark Rada Date: Fri, 2 Apr 2010 20:35:05 -0400 Subject: Gitweb: add support for minifying gitweb.css The build system added support minifying gitweb.js through a JavaScript minifier, but most minifiers come with support for minifying CSS files as well, so we should use it if we can. This patch will add the same facilities to gitweb.css that gitweb.js has for minification. That does not mean that they will use the same minifier though, as it is not safe to assume that all JavaScript minifiers will also minify CSS files. This patch also adds the GITWEB_PROGRAMS variable to the Makefile to keep a list of potential gitweb dependencies separate from OTHER_PROGRAMS when we need to know just the gitweb dependencies. Though the bandwidth savings will not be as dramatic as with the JavaScript minifier, every byte saved is important. Signed-off-by: Mark Rada Signed-off-by: Junio C Hamano --- gitweb/Makefile | 28 +++++++++++++++++++++------- 1 file changed, 21 insertions(+), 7 deletions(-) (limited to 'gitweb/Makefile') diff --git a/gitweb/Makefile b/gitweb/Makefile index c9eb1ee667..fffe700760 100644 --- a/gitweb/Makefile +++ b/gitweb/Makefile @@ -6,13 +6,17 @@ all:: # Define JSMIN to point to JavaScript minifier that functions as # a filter to have gitweb.js minified. # +# Define CSSMIN to point to a CSS minifier in order to generate a minified +# version of gitweb.css +# prefix ?= $(HOME) bindir ?= $(prefix)/bin RM ?= rm -f -# JavaScript minifier invocation that can function as filter +# JavaScript/CSS minifier invocation that can function as filter JSMIN ?= +CSSMIN ?= # default configuration for gitweb GITWEB_CONFIG = gitweb_config.perl @@ -26,7 +30,11 @@ GITWEB_STRICT_EXPORT = GITWEB_BASE_URL = GITWEB_LIST = GITWEB_HOMETEXT = indextext.html +ifdef CSSMIN +GITWEB_CSS = gitweb.min.css +else GITWEB_CSS = gitweb.css +endif GITWEB_LOGO = git-logo.png GITWEB_FAVICON = git-favicon.png ifdef JSMIN @@ -84,13 +92,14 @@ endif all:: gitweb.cgi +FILES = gitweb.cgi ifdef JSMIN -FILES=gitweb.cgi gitweb.min.js -gitweb.cgi: gitweb.perl gitweb.min.js -else # !JSMIN -FILES=gitweb.cgi -gitweb.cgi: gitweb.perl -endif # JSMIN +FILES += gitweb.min.js +endif +ifdef CSSMIN +FILES += gitweb.min.css +endif +gitweb.cgi: gitweb.perl $(GITWEB_JS) $(GITWEB_CSS) gitweb.cgi: $(QUIET_GEN)$(RM) $@ $@+ && \ @@ -123,6 +132,11 @@ gitweb.min.js: gitweb.js $(QUIET_GEN)$(JSMIN) <$< >$@ endif # JSMIN +ifdef CSSMIN +gitweb.min.css: gitweb.css + $(QUIET_GEN)$(CSSMIN) <$ >$@ +endif + clean: $(RM) $(FILES) -- cgit v1.3 From bb4bbf75829d1a6c021e34943ff4e942f064bb55 Mon Sep 17 00:00:00 2001 From: Mark Rada Date: Fri, 2 Apr 2010 20:35:54 -0400 Subject: Gitweb: add autoconfigure support for minifiers This will allow users to set a JavaScript/CSS minifier when/if they run the autoconfigure script while building git. Signed-off-by: Mark Rada Signed-off-by: Junio C Hamano --- Makefile | 4 ---- configure.ac | 20 ++++++++++++++++++++ gitweb/Makefile | 14 ++------------ 3 files changed, 22 insertions(+), 16 deletions(-) (limited to 'gitweb/Makefile') diff --git a/Makefile b/Makefile index 9bebcf1c44..2c36def3ec 100644 --- a/Makefile +++ b/Makefile @@ -282,10 +282,6 @@ lib = lib # DESTDIR= pathsep = : -# JavaScript/CSS minifier invocation that can function as filter -JSMIN = -CSSMIN = - export prefix bindir sharedir sysconfdir CC = gcc diff --git a/configure.ac b/configure.ac index 914ae5759f..f4d7372ef8 100644 --- a/configure.ac +++ b/configure.ac @@ -179,6 +179,26 @@ fi], AC_MSG_NOTICE([Will try -pthread then -lpthread to enable POSIX Threads.]) ]) +# Define option to enable JavaScript minification +AC_ARG_ENABLE([jsmin], +[AS_HELP_STRING([--enable-jsmin=PATH], + [PATH is the name of a JavaScript minifier or the absolute path to one.])], +[ + JSMIN=$enableval; + AC_MSG_NOTICE([Setting JSMIN to '$JSMIN' to enable JavaScript minifying]) + GIT_CONF_APPEND_LINE(JSMIN=$enableval); +]) + +# Define option to enable CSS minification +AC_ARG_ENABLE([cssmin], +[AS_HELP_STRING([--enable-cssmin=PATH], + [PATH is the name of a CSS minifier or the absolute path to one.])], +[ + CSSMIN=$enableval; + AC_MSG_NOTICE([Setting CSSMIN to '$CSSMIN' to enable CSS minifying]) + GIT_CONF_APPEND_LINE(CSSMIN=$enableval); +]) + ## Site configuration (override autodetection) ## --with-PACKAGE[=ARG] and --without-PACKAGE AC_MSG_NOTICE([CHECKS for site configuration]) diff --git a/gitweb/Makefile b/gitweb/Makefile index fffe700760..ffee4bd1e3 100644 --- a/gitweb/Makefile +++ b/gitweb/Makefile @@ -14,10 +14,6 @@ prefix ?= $(HOME) bindir ?= $(prefix)/bin RM ?= rm -f -# JavaScript/CSS minifier invocation that can function as filter -JSMIN ?= -CSSMIN ?= - # default configuration for gitweb GITWEB_CONFIG = gitweb_config.perl GITWEB_CONFIG_SYSTEM = /etc/gitweb.conf @@ -30,18 +26,10 @@ GITWEB_STRICT_EXPORT = GITWEB_BASE_URL = GITWEB_LIST = GITWEB_HOMETEXT = indextext.html -ifdef CSSMIN -GITWEB_CSS = gitweb.min.css -else GITWEB_CSS = gitweb.css -endif GITWEB_LOGO = git-logo.png GITWEB_FAVICON = git-favicon.png -ifdef JSMIN -GITWEB_JS = gitweb.min.js -else GITWEB_JS = gitweb.js -endif GITWEB_SITE_HEADER = GITWEB_SITE_FOOTER = @@ -95,9 +83,11 @@ all:: gitweb.cgi FILES = gitweb.cgi ifdef JSMIN FILES += gitweb.min.js +GITWEB_JS = gitweb.min.js endif ifdef CSSMIN FILES += gitweb.min.css +GITWEB_CSS = gitweb.min.css endif gitweb.cgi: gitweb.perl $(GITWEB_JS) $(GITWEB_CSS) -- cgit v1.3 From 8de096b6718048e84ee88e604a94b22937a0758e Mon Sep 17 00:00:00 2001 From: Junio C Hamano Date: Thu, 15 Apr 2010 08:57:18 -0400 Subject: gitweb: simplify gitweb.min.* generation and clean-up rules GITWEB_CSS and GITWEB_JS are meant to be "what URI should the installed cgi script use to refer to the stylesheet and JavaScript", never "this is the name of the file we are building". Don't use them to decide what file to build minified versions in. While we are at it, lose FILES that is used only for "clean" target in a misguided way. "make clean" should try to remove all the potential build artifacts regardless of a minor configuration change. Instead of trying to remove only the build product "make clean" would have created if it were run without "clean", explicitly list the three potential build products for removal. Tested-by: Mark Rada Signed-off-by: Junio C Hamano --- gitweb/Makefile | 75 +++++++++++++++++++++++++++++---------------------------- 1 file changed, 38 insertions(+), 37 deletions(-) (limited to 'gitweb/Makefile') diff --git a/gitweb/Makefile b/gitweb/Makefile index ffee4bd1e3..f2e1d92fbb 100644 --- a/gitweb/Makefile +++ b/gitweb/Makefile @@ -80,54 +80,55 @@ endif all:: gitweb.cgi -FILES = gitweb.cgi ifdef JSMIN -FILES += gitweb.min.js GITWEB_JS = gitweb.min.js +all:: gitweb.min.js +gitweb.min.js: gitweb.js GITWEB-BUILD-OPTIONS + $(QUIET_GEN)$(JSMIN) <$< >$@ endif + ifdef CSSMIN -FILES += gitweb.min.css GITWEB_CSS = gitweb.min.css +all:: gitweb.min.css +gitweb.min.css: gitweb.css GITWEB-BUILD-OPTIONS + $(QUIET_GEN)$(CSSMIN) <$ >$@ endif -gitweb.cgi: gitweb.perl $(GITWEB_JS) $(GITWEB_CSS) -gitweb.cgi: +GITWEB_REPLACE = \ + -e 's|++GIT_VERSION++|$(GIT_VERSION)|g' \ + -e 's|++GIT_BINDIR++|$(bindir)|g' \ + -e 's|++GITWEB_CONFIG++|$(GITWEB_CONFIG)|g' \ + -e 's|++GITWEB_CONFIG_SYSTEM++|$(GITWEB_CONFIG_SYSTEM)|g' \ + -e 's|++GITWEB_HOME_LINK_STR++|$(GITWEB_HOME_LINK_STR)|g' \ + -e 's|++GITWEB_SITENAME++|$(GITWEB_SITENAME)|g' \ + -e 's|++GITWEB_PROJECTROOT++|$(GITWEB_PROJECTROOT)|g' \ + -e 's|"++GITWEB_PROJECT_MAXDEPTH++"|$(GITWEB_PROJECT_MAXDEPTH)|g' \ + -e 's|++GITWEB_EXPORT_OK++|$(GITWEB_EXPORT_OK)|g' \ + -e 's|++GITWEB_STRICT_EXPORT++|$(GITWEB_STRICT_EXPORT)|g' \ + -e 's|++GITWEB_BASE_URL++|$(GITWEB_BASE_URL)|g' \ + -e 's|++GITWEB_LIST++|$(GITWEB_LIST)|g' \ + -e 's|++GITWEB_HOMETEXT++|$(GITWEB_HOMETEXT)|g' \ + -e 's|++GITWEB_CSS++|$(GITWEB_CSS)|g' \ + -e 's|++GITWEB_LOGO++|$(GITWEB_LOGO)|g' \ + -e 's|++GITWEB_FAVICON++|$(GITWEB_FAVICON)|g' \ + -e 's|++GITWEB_JS++|$(GITWEB_JS)|g' \ + -e 's|++GITWEB_SITE_HEADER++|$(GITWEB_SITE_HEADER)|g' \ + -e 's|++GITWEB_SITE_FOOTER++|$(GITWEB_SITE_FOOTER)|g' + +GITWEB-BUILD-OPTIONS: FORCE + @rm -f $@+ + @echo "x" '$(PERL_PATH_SQ)' $(GITWEB_REPLACE) "$(JSMIN)|$(CSSMIN)" >$@+ + @cmp -s $@+ $@ && rm -f $@+ || mv -f $@+ $@ + +gitweb.cgi: gitweb.perl GITWEB-BUILD-OPTIONS $(QUIET_GEN)$(RM) $@ $@+ && \ sed -e '1s|#!.*perl|#!$(PERL_PATH_SQ)|' \ - -e 's|++GIT_VERSION++|$(GIT_VERSION)|g' \ - -e 's|++GIT_BINDIR++|$(bindir)|g' \ - -e 's|++GITWEB_CONFIG++|$(GITWEB_CONFIG)|g' \ - -e 's|++GITWEB_CONFIG_SYSTEM++|$(GITWEB_CONFIG_SYSTEM)|g' \ - -e 's|++GITWEB_HOME_LINK_STR++|$(GITWEB_HOME_LINK_STR)|g' \ - -e 's|++GITWEB_SITENAME++|$(GITWEB_SITENAME)|g' \ - -e 's|++GITWEB_PROJECTROOT++|$(GITWEB_PROJECTROOT)|g' \ - -e 's|"++GITWEB_PROJECT_MAXDEPTH++"|$(GITWEB_PROJECT_MAXDEPTH)|g' \ - -e 's|++GITWEB_EXPORT_OK++|$(GITWEB_EXPORT_OK)|g' \ - -e 's|++GITWEB_STRICT_EXPORT++|$(GITWEB_STRICT_EXPORT)|g' \ - -e 's|++GITWEB_BASE_URL++|$(GITWEB_BASE_URL)|g' \ - -e 's|++GITWEB_LIST++|$(GITWEB_LIST)|g' \ - -e 's|++GITWEB_HOMETEXT++|$(GITWEB_HOMETEXT)|g' \ - -e 's|++GITWEB_CSS++|$(GITWEB_CSS)|g' \ - -e 's|++GITWEB_LOGO++|$(GITWEB_LOGO)|g' \ - -e 's|++GITWEB_FAVICON++|$(GITWEB_FAVICON)|g' \ - -e 's|++GITWEB_JS++|$(GITWEB_JS)|g' \ - -e 's|++GITWEB_SITE_HEADER++|$(GITWEB_SITE_HEADER)|g' \ - -e 's|++GITWEB_SITE_FOOTER++|$(GITWEB_SITE_FOOTER)|g' \ - $< >$@+ && \ + $(GITWEB_REPLACE) $< >$@+ && \ chmod +x $@+ && \ mv $@+ $@ -ifdef JSMIN -gitweb.min.js: gitweb.js - $(QUIET_GEN)$(JSMIN) <$< >$@ -endif # JSMIN - -ifdef CSSMIN -gitweb.min.css: gitweb.css - $(QUIET_GEN)$(CSSMIN) <$ >$@ -endif - clean: - $(RM) $(FILES) + $(RM) gitweb.cgi gitweb.min.js gitweb.min.css GITWEB-BUILD-OPTIONS + +.PHONY: all clean .FORCE-GIT-VERSION-FILE FORCE -.PHONY: all clean .FORCE-GIT-VERSION-FILE -- cgit v1.3 From 152d94348f6a38eb7cb5f4af8397f51ba06ddffb Mon Sep 17 00:00:00 2001 From: Jakub Narebski Date: Sat, 1 May 2010 22:36:15 +0200 Subject: gitweb: Create install target for gitweb in Makefile Installing gitweb is now as easy as # make gitwebdir=/var/www/cgi-bin gitweb-install ;# as root The gitweb/INSTALL file was updated accordingly, to make use of this new target. Fix shell quoting, i.e. setting bindir_SQ etc., in gitweb/Makefile. Those variables were not used previously. Signed-off-by: Jakub Narebski Signed-off-by: Junio C Hamano --- Makefile | 3 +++ gitweb/INSTALL | 11 ++++------- gitweb/Makefile | 32 ++++++++++++++++++++++++++++---- 3 files changed, 35 insertions(+), 11 deletions(-) (limited to 'gitweb/Makefile') diff --git a/Makefile b/Makefile index 910f4713ef..dab5a14e41 100644 --- a/Makefile +++ b/Makefile @@ -2004,6 +2004,9 @@ endif done; } && \ ./check_bindir "z$$bindir" "z$$execdir" "$$bindir/git-add$X" +install-gitweb: + $(MAKE) -C gitweb install + install-doc: $(MAKE) -C Documentation install diff --git a/gitweb/INSTALL b/gitweb/INSTALL index 208e4a8f05..d484d76b75 100644 --- a/gitweb/INSTALL +++ b/gitweb/INSTALL @@ -6,10 +6,8 @@ First you have to generate gitweb.cgi from gitweb.perl using gitweb.css, git-logo.png and git-favicon.png) to their destination. For example if git was (or is) installed with /usr prefix, you can do - $ make prefix=/usr gitweb ;# as yourself - # cp gitweb/*.cgi gitweb/*.css \ - gitweb/*.js gitweb/*.png \ - /var/www/cgi-bin/ ;# as root + $ make prefix=/usr gitweb ;# as yourself + # make gitwebdir=/var/www/cgi-bin install-gitweb ;# as root Alternatively you can use autoconf generated ./configure script to set up path to git binaries (via config.mak.autogen), so you can write @@ -18,9 +16,8 @@ instead $ make configure ;# as yourself $ ./configure --prefix=/usr ;# as yourself $ make gitweb ;# as yourself - # cp gitweb/*.cgi gitweb/*.css \ - gitweb/*.js gitweb/*.png \ - /var/www/cgi-bin/ ;# as root + # make gitwebdir=/var/www/cgi-bin \ + install-gitweb ;# as root The above example assumes that your web server is configured to run [executable] files in /var/www/cgi-bin/ as server scripts (as CGI diff --git a/gitweb/Makefile b/gitweb/Makefile index f2e1d92fbb..935d2d2e07 100644 --- a/gitweb/Makefile +++ b/gitweb/Makefile @@ -12,7 +12,10 @@ all:: prefix ?= $(HOME) bindir ?= $(prefix)/bin +gitwebdir ?= /var/www/cgi-bin + RM ?= rm -f +INSTALL ?= install # default configuration for gitweb GITWEB_CONFIG = gitweb_config.perl @@ -49,9 +52,11 @@ SHELL_PATH ?= $(SHELL) PERL_PATH ?= /usr/bin/perl # Shell quote; -bindir_SQ = $(subst ','\'',$(bindir)) #' -SHELL_PATH_SQ = $(subst ','\'',$(SHELL_PATH)) #' -PERL_PATH_SQ = $(subst ','\'',$(PERL_PATH)) #' +bindir_SQ = $(subst ','\'',$(bindir))#' +gitwebdir_SQ = $(subst ','\'',$(gitwebdir))#' +SHELL_PATH_SQ = $(subst ','\'',$(SHELL_PATH))#' +PERL_PATH_SQ = $(subst ','\'',$(PERL_PATH))#' +DESTDIR_SQ = $(subst ','\'',$(DESTDIR))#' # Quiet generation (unless V=1) QUIET_SUBDIR0 = +$(MAKE) -C # space to separate -C and subdir @@ -80,20 +85,30 @@ endif all:: gitweb.cgi +GITWEB_PROGRAMS = gitweb.cgi + ifdef JSMIN +GITWEB_FILES += gitweb.min.js GITWEB_JS = gitweb.min.js all:: gitweb.min.js gitweb.min.js: gitweb.js GITWEB-BUILD-OPTIONS $(QUIET_GEN)$(JSMIN) <$< >$@ +else +GITWEB_FILES += gitweb.js endif ifdef CSSMIN +GITWEB_FILES += gitweb.min.css GITWEB_CSS = gitweb.min.css all:: gitweb.min.css gitweb.min.css: gitweb.css GITWEB-BUILD-OPTIONS $(QUIET_GEN)$(CSSMIN) <$ >$@ +else +GITWEB_FILES += gitweb.css endif +GITWEB_FILES += git-logo.png git-favicon.png + GITWEB_REPLACE = \ -e 's|++GIT_VERSION++|$(GIT_VERSION)|g' \ -e 's|++GIT_BINDIR++|$(bindir)|g' \ @@ -127,8 +142,17 @@ gitweb.cgi: gitweb.perl GITWEB-BUILD-OPTIONS chmod +x $@+ && \ mv $@+ $@ +### Installation rules + +install: all + $(INSTALL) -d -m 755 '$(DESTDIR_SQ)$(gitwebdir_SQ)' + $(INSTALL) -m 755 $(GITWEB_PROGRAMS) '$(DESTDIR_SQ)$(gitwebdir_SQ)' + $(INSTALL) -m 644 $(GITWEB_FILES) '$(DESTDIR_SQ)$(gitwebdir_SQ)' + +### Cleaning rules + clean: $(RM) gitweb.cgi gitweb.min.js gitweb.min.css GITWEB-BUILD-OPTIONS -.PHONY: all clean .FORCE-GIT-VERSION-FILE FORCE +.PHONY: all clean install .FORCE-GIT-VERSION-FILE FORCE -- cgit v1.3 From 18d05328f3333e63bfffa65ac887ea16de60918c Mon Sep 17 00:00:00 2001 From: Pavan Kumar Sunkara Date: Fri, 28 May 2010 11:55:49 +0530 Subject: gitweb: Move static files into seperate subdirectory Create a new subdirectory called 'static' in gitweb/, and move all static files required by gitweb.cgi when running, which means styles, images and Javascript code. This should make gitweb more readable and easier to maintain. Update t/gitweb-lib.sh to reflect this change.The install-gitweb now also include moving of static files into 'static' subdirectory in target directory: update Makefile, gitweb's INSTALL, README and Makefile accordingly. Signed-off-by: Pavan Kumar Sunkara Mentored-by: Christian Couder Mentored-by: Petr Baudis Acked-by: Jakub Narebski Acked-by: Petr Baudis Signed-off-by: Junio C Hamano --- Makefile | 18 +- gitweb/INSTALL | 19 +- gitweb/Makefile | 40 +- gitweb/README | 14 +- gitweb/git-favicon.png | Bin 115 -> 0 bytes gitweb/git-logo.png | Bin 207 -> 0 bytes gitweb/gitweb.css | 574 --------------------------- gitweb/gitweb.js | 875 ------------------------------------------ gitweb/static/git-favicon.png | Bin 0 -> 115 bytes gitweb/static/git-logo.png | Bin 0 -> 207 bytes gitweb/static/gitweb.css | 574 +++++++++++++++++++++++++++ gitweb/static/gitweb.js | 875 ++++++++++++++++++++++++++++++++++++++++++ t/gitweb-lib.sh | 6 +- 13 files changed, 1499 insertions(+), 1496 deletions(-) delete mode 100644 gitweb/git-favicon.png delete mode 100644 gitweb/git-logo.png delete mode 100644 gitweb/gitweb.css delete mode 100644 gitweb/gitweb.js create mode 100644 gitweb/static/git-favicon.png create mode 100644 gitweb/static/git-logo.png create mode 100644 gitweb/static/gitweb.css create mode 100644 gitweb/static/gitweb.js (limited to 'gitweb/Makefile') diff --git a/Makefile b/Makefile index 07cab8f6a4..1ec90f53fa 100644 --- a/Makefile +++ b/Makefile @@ -1578,32 +1578,32 @@ gitweb: $(QUIET_SUBDIR0)gitweb $(QUIET_SUBDIR1) all ifdef JSMIN -GITWEB_PROGRAMS += gitweb/gitweb.min.js -GITWEB_JS = gitweb/gitweb.min.js +GITWEB_PROGRAMS += gitweb/static/gitweb.min.js +GITWEB_JS = gitweb/static/gitweb.min.js else -GITWEB_JS = gitweb/gitweb.js +GITWEB_JS = gitweb/static/gitweb.js endif ifdef CSSMIN -GITWEB_PROGRAMS += gitweb/gitweb.min.css -GITWEB_CSS = gitweb/gitweb.min.css +GITWEB_PROGRAMS += gitweb/static/gitweb.min.css +GITWEB_CSS = gitweb/static/gitweb.min.css else -GITWEB_CSS = gitweb/gitweb.css +GITWEB_CSS = gitweb/static/gitweb.css endif OTHER_PROGRAMS += gitweb/gitweb.cgi $(GITWEB_PROGRAMS) gitweb/gitweb.cgi: gitweb/gitweb.perl $(GITWEB_PROGRAMS) $(QUIET_SUBDIR0)gitweb $(QUIET_SUBDIR1) $(patsubst gitweb/%,%,$@) ifdef JSMIN -gitweb/gitweb.min.js: gitweb/gitweb.js +gitweb/static/gitweb.min.js: gitweb/static/gitweb.js $(QUIET_SUBDIR0)gitweb $(QUIET_SUBDIR1) $(patsubst gitweb/%,%,$@) endif # JSMIN ifdef CSSMIN -gitweb/gitweb.min.css: gitweb/gitweb.css +gitweb/static/gitweb.min.css: gitweb/static/gitweb.css $(QUIET_SUBDIR0)gitweb $(QUIET_SUBDIR1) $(patsubst gitweb/%,%,$@) endif # CSSMIN -git-instaweb: git-instaweb.sh gitweb/gitweb.cgi gitweb/gitweb.css gitweb/gitweb.js +git-instaweb: git-instaweb.sh gitweb/gitweb.cgi gitweb/static/gitweb.css gitweb/static/gitweb.js $(QUIET_GEN)$(RM) $@ $@+ && \ sed -e '1s|#!.*/sh|#!$(SHELL_PATH_SQ)|' \ -e 's/@@GIT_VERSION@@/$(GIT_VERSION)/g' \ diff --git a/gitweb/INSTALL b/gitweb/INSTALL index d484d76b75..823053173c 100644 --- a/gitweb/INSTALL +++ b/gitweb/INSTALL @@ -2,9 +2,10 @@ GIT web Interface (gitweb) Installation ======================================= First you have to generate gitweb.cgi from gitweb.perl using -"make gitweb", then copy appropriate files (gitweb.cgi, gitweb.js, -gitweb.css, git-logo.png and git-favicon.png) to their destination. -For example if git was (or is) installed with /usr prefix, you can do +"make gitweb", then "make install-gitweb" appropriate files +(gitweb.cgi, gitweb.js, gitweb.css, git-logo.png and git-favicon.png) +to their destination. For example if git was (or is) installed with +/usr prefix and gitwebdir is /var/www/cgi-bin, you can do $ make prefix=/usr gitweb ;# as yourself # make gitwebdir=/var/www/cgi-bin install-gitweb ;# as root @@ -81,16 +82,14 @@ Build example minifiers, you can do make GITWEB_PROJECTROOT="/home/local/scm" \ - GITWEB_JS="/gitweb/gitweb.js" \ - GITWEB_CSS="/gitweb/gitweb.css" \ - GITWEB_LOGO="/gitweb/git-logo.png" \ - GITWEB_FAVICON="/gitweb/git-favicon.png" \ + GITWEB_JS="gitweb/static/gitweb.js" \ + GITWEB_CSS="gitweb/static/gitweb.css" \ + GITWEB_LOGO="gitweb/static/git-logo.png" \ + GITWEB_FAVICON="gitweb/static/git-favicon.png" \ bindir=/usr/local/bin \ gitweb - cp -fv gitweb/gitweb.{cgi,js,css} \ - gitweb/git-{favicon,logo}.png \ - /var/www/cgi-bin/gitweb/ + make gitwebdir=/var/www/cgi-bin/gitweb install-gitweb Gitweb config file diff --git a/gitweb/Makefile b/gitweb/Makefile index 935d2d2e07..d2584fedd8 100644 --- a/gitweb/Makefile +++ b/gitweb/Makefile @@ -4,10 +4,10 @@ all:: # Define V=1 to have a more verbose compile. # # Define JSMIN to point to JavaScript minifier that functions as -# a filter to have gitweb.js minified. +# a filter to have static/gitweb.js minified. # # Define CSSMIN to point to a CSS minifier in order to generate a minified -# version of gitweb.css +# version of static/gitweb.css # prefix ?= $(HOME) @@ -29,10 +29,10 @@ GITWEB_STRICT_EXPORT = GITWEB_BASE_URL = GITWEB_LIST = GITWEB_HOMETEXT = indextext.html -GITWEB_CSS = gitweb.css -GITWEB_LOGO = git-logo.png -GITWEB_FAVICON = git-favicon.png -GITWEB_JS = gitweb.js +GITWEB_CSS = static/gitweb.css +GITWEB_LOGO = static/git-logo.png +GITWEB_FAVICON = static/git-favicon.png +GITWEB_JS = static/gitweb.js GITWEB_SITE_HEADER = GITWEB_SITE_FOOTER = @@ -54,6 +54,7 @@ PERL_PATH ?= /usr/bin/perl # Shell quote; bindir_SQ = $(subst ','\'',$(bindir))#' gitwebdir_SQ = $(subst ','\'',$(gitwebdir))#' +gitwebstaticdir_SQ = $(subst ','\'',$(gitwebdir)/static)#' SHELL_PATH_SQ = $(subst ','\'',$(SHELL_PATH))#' PERL_PATH_SQ = $(subst ','\'',$(PERL_PATH))#' DESTDIR_SQ = $(subst ','\'',$(DESTDIR))#' @@ -88,26 +89,26 @@ all:: gitweb.cgi GITWEB_PROGRAMS = gitweb.cgi ifdef JSMIN -GITWEB_FILES += gitweb.min.js -GITWEB_JS = gitweb.min.js -all:: gitweb.min.js -gitweb.min.js: gitweb.js GITWEB-BUILD-OPTIONS +GITWEB_FILES += static/gitweb.min.js +GITWEB_JS = static/gitweb.min.js +all:: static/gitweb.min.js +static/gitweb.min.js: static/gitweb.js GITWEB-BUILD-OPTIONS $(QUIET_GEN)$(JSMIN) <$< >$@ else -GITWEB_FILES += gitweb.js +GITWEB_FILES += static/gitweb.js endif ifdef CSSMIN -GITWEB_FILES += gitweb.min.css -GITWEB_CSS = gitweb.min.css -all:: gitweb.min.css -gitweb.min.css: gitweb.css GITWEB-BUILD-OPTIONS +GITWEB_FILES += static/gitweb.min.css +GITWEB_CSS = static/gitweb.min.css +all:: static/gitweb.min.css +static/gitweb.min.css: static/gitweb.css GITWEB-BUILD-OPTIONS $(QUIET_GEN)$(CSSMIN) <$ >$@ else -GITWEB_FILES += gitweb.css +GITWEB_FILES += static/gitweb.css endif -GITWEB_FILES += git-logo.png git-favicon.png +GITWEB_FILES += static/git-logo.png static/git-favicon.png GITWEB_REPLACE = \ -e 's|++GIT_VERSION++|$(GIT_VERSION)|g' \ @@ -147,12 +148,13 @@ gitweb.cgi: gitweb.perl GITWEB-BUILD-OPTIONS install: all $(INSTALL) -d -m 755 '$(DESTDIR_SQ)$(gitwebdir_SQ)' $(INSTALL) -m 755 $(GITWEB_PROGRAMS) '$(DESTDIR_SQ)$(gitwebdir_SQ)' - $(INSTALL) -m 644 $(GITWEB_FILES) '$(DESTDIR_SQ)$(gitwebdir_SQ)' + $(INSTALL) -d -m 755 '$(DESTDIR_SQ)$(gitwebstaticdir_SQ)' + $(INSTALL) -m 644 $(GITWEB_FILES) '$(DESTDIR_SQ)$(gitwebstaticdir_SQ)' ### Cleaning rules clean: - $(RM) gitweb.cgi gitweb.min.js gitweb.min.css GITWEB-BUILD-OPTIONS + $(RM) gitweb.cgi static/gitweb.min.js static/gitweb.min.css GITWEB-BUILD-OPTIONS .PHONY: all clean install .FORCE-GIT-VERSION-FILE FORCE diff --git a/gitweb/README b/gitweb/README index 71742b335d..0e19be8d21 100644 --- a/gitweb/README +++ b/gitweb/README @@ -80,24 +80,26 @@ You can specify the following configuration variables when building GIT: Points to the location where you put gitweb.css on your web server (or to be more generic, the URI of gitweb stylesheet). Relative to the base URI of gitweb. Note that you can setup multiple stylesheets from - the gitweb config file. [Default: gitweb.css (or gitweb.min.css if the - CSSMIN variable is defined / CSS minifier is used)] + the gitweb config file. [Default: static/gitweb.css (or + static/gitweb.min.css if the CSSMIN variable is defined / CSS minifier + is used)] * GITWEB_LOGO Points to the location where you put git-logo.png on your web server (or to be more generic URI of logo, 72x27 size, displayed in top right corner of each gitweb page, and used as logo for Atom feed). Relative - to base URI of gitweb. [Default: git-logo.png] + to base URI of gitweb. [Default: static/git-logo.png] * GITWEB_FAVICON Points to the location where you put git-favicon.png on your web server (or to be more generic URI of favicon, assumed to be image/png type; web browsers that support favicons (website icons) may display them in the browser's URL bar and next to site name in bookmarks). Relative - to base URI of gitweb. [Default: git-favicon.png] + to base URI of gitweb. [Default: static/git-favicon.png] * GITWEB_JS Points to the localtion where you put gitweb.js on your web server (or to be more generic URI of JavaScript code used by gitweb). - Relative to base URI of gitweb. [Default: gitweb.js (or gitweb.min.js - if JSMIN build variable is defined / JavaScript minifier is used)] + Relative to base URI of gitweb. [Default: static/gitweb.js (or + static/gitweb.min.js if JSMIN build variable is defined / JavaScript + minifier is used)] * GITWEB_CONFIG This Perl file will be loaded using 'do' and can be used to override any of the options above as well as some other options -- see the "Runtime diff --git a/gitweb/git-favicon.png b/gitweb/git-favicon.png deleted file mode 100644 index aae35a70e7..0000000000 Binary files a/gitweb/git-favicon.png and /dev/null differ diff --git a/gitweb/git-logo.png b/gitweb/git-logo.png deleted file mode 100644 index f4ede2e944..0000000000 Binary files a/gitweb/git-logo.png and /dev/null differ diff --git a/gitweb/gitweb.css b/gitweb/gitweb.css deleted file mode 100644 index 50067f2e0d..0000000000 --- a/gitweb/gitweb.css +++ /dev/null @@ -1,574 +0,0 @@ -body { - font-family: sans-serif; - font-size: small; - border: solid #d9d8d1; - border-width: 1px; - margin: 10px; - background-color: #ffffff; - color: #000000; -} - -a { - color: #0000cc; -} - -a:hover, a:visited, a:active { - color: #880000; -} - -span.cntrl { - border: dashed #aaaaaa; - border-width: 1px; - padding: 0px 2px 0px 2px; - margin: 0px 2px 0px 2px; -} - -img.logo { - float: right; - border-width: 0px; -} - -img.avatar { - vertical-align: middle; -} - -a.list img.avatar { - border-style: none; -} - -div.page_header { - height: 25px; - padding: 8px; - font-size: 150%; - font-weight: bold; - background-color: #d9d8d1; -} - -div.page_header a:visited, a.header { - color: #0000cc; -} - -div.page_header a:hover { - color: #880000; -} - -div.page_nav { - padding: 8px; -} - -div.page_nav a:visited { - color: #0000cc; -} - -div.page_path { - padding: 8px; - font-weight: bold; - border: solid #d9d8d1; - border-width: 0px 0px 1px; -} - -div.page_footer { - height: 17px; - padding: 4px 8px; - background-color: #d9d8d1; -} - -div.page_footer_text { - float: left; - color: #555555; - font-style: italic; -} - -div#generating_info { - margin: 4px; - font-size: smaller; - text-align: center; - color: #505050; -} - -div.page_body { - padding: 8px; - font-family: monospace; -} - -div.title, a.title { - display: block; - padding: 6px 8px; - font-weight: bold; - background-color: #edece6; - text-decoration: none; - color: #000000; -} - -div.readme { - padding: 8px; -} - -a.title:hover { - background-color: #d9d8d1; -} - -div.title_text { - padding: 6px 0px; - border: solid #d9d8d1; - border-width: 0px 0px 1px; - font-family: monospace; -} - -div.log_body { - padding: 8px 8px 8px 150px; -} - -span.age { - position: relative; - float: left; - width: 142px; - font-style: italic; -} - -span.signoff { - color: #888888; -} - -div.log_link { - padding: 0px 8px; - font-size: 70%; - font-family: sans-serif; - font-style: normal; - position: relative; - float: left; - width: 136px; -} - -div.list_head { - padding: 6px 8px 4px; - border: solid #d9d8d1; - border-width: 1px 0px 0px; - font-style: italic; -} - -.author_date, .author { - font-style: italic; -} - -div.author_date { - padding: 8px; - border: solid #d9d8d1; - border-width: 0px 0px 1px 0px; -} - -a.list { - text-decoration: none; - color: #000000; -} - -a.subject, a.name { - font-weight: bold; -} - -table.tags a.subject { - font-weight: normal; -} - -a.list:hover { - text-decoration: underline; - color: #880000; -} - -a.text { - text-decoration: none; - color: #0000cc; -} - -a.text:visited { - text-decoration: none; - color: #880000; -} - -a.text:hover { - text-decoration: underline; - color: #880000; -} - -table { - padding: 8px 4px; - border-spacing: 0; -} - -table.diff_tree { - font-family: monospace; -} - -table.combined.diff_tree th { - text-align: center; -} - -table.combined.diff_tree td { - padding-right: 24px; -} - -table.combined.diff_tree th.link, -table.combined.diff_tree td.link { - padding: 0px 2px; -} - -table.combined.diff_tree td.nochange a { - color: #6666ff; -} - -table.combined.diff_tree td.nochange a:hover, -table.combined.diff_tree td.nochange a:visited { - color: #d06666; -} - -table.blame { - border-collapse: collapse; -} - -table.blame td { - padding: 0px 5px; - font-size: 100%; - vertical-align: top; -} - -th { - padding: 2px 5px; - font-size: 100%; - text-align: left; -} - -/* do not change row style on hover for 'blame' view */ -tr.light, -table.blame .light:hover { - background-color: #ffffff; -} - -tr.dark, -table.blame .dark:hover { - background-color: #f6f6f0; -} - -/* currently both use the same, but it can change */ -tr.light:hover, -tr.dark:hover { - background-color: #edece6; -} - -/* boundary commits in 'blame' view */ -/* and commits without "previous" */ -tr.boundary td.sha1, -tr.no-previous td.linenr { - font-weight: bold; -} - -/* for 'blame_incremental', during processing */ -tr.color1 { background-color: #f6fff6; } -tr.color2 { background-color: #f6f6ff; } -tr.color3 { background-color: #fff6f6; } - -td { - padding: 2px 5px; - font-size: 100%; - vertical-align: top; -} - -td.link, td.selflink { - padding: 2px 5px; - font-family: sans-serif; - font-size: 70%; -} - -td.selflink { - padding-right: 0px; -} - -td.sha1 { - font-family: monospace; -} - -.error { - color: red; - background-color: yellow; -} - -td.current_head { - text-decoration: underline; -} - -table.diff_tree span.file_status.new { - color: #008000; -} - -table.diff_tree span.file_status.deleted { - color: #c00000; -} - -table.diff_tree span.file_status.moved, -table.diff_tree span.file_status.mode_chnge { - color: #777777; -} - -table.diff_tree span.file_status.copied { - color: #70a070; -} - -/* noage: "No commits" */ -table.project_list td.noage { - color: #808080; - font-style: italic; -} - -/* age2: 60*60*24*2 <= age */ -table.project_list td.age2, table.blame td.age2 { - font-style: italic; -} - -/* age1: 60*60*2 <= age < 60*60*24*2 */ -table.project_list td.age1 { - color: #009900; - font-style: italic; -} - -table.blame td.age1 { - color: #009900; - background: transparent; -} - -/* age0: age < 60*60*2 */ -table.project_list td.age0 { - color: #009900; - font-style: italic; - font-weight: bold; -} - -table.blame td.age0 { - color: #009900; - background: transparent; - font-weight: bold; -} - -td.pre, div.pre, div.diff { - font-family: monospace; - font-size: 12px; - white-space: pre; -} - -td.mode { - font-family: monospace; -} - -/* progress of blame_interactive */ -div#progress_bar { - height: 2px; - margin-bottom: -2px; - background-color: #d8d9d0; -} -div#progress_info { - float: right; - text-align: right; -} - -/* format of (optional) objects size in 'tree' view */ -td.size { - font-family: monospace; - text-align: right; -} - -/* styling of diffs (patchsets): commitdiff and blobdiff views */ -div.diff.header, -div.diff.extended_header { - white-space: normal; -} - -div.diff.header { - font-weight: bold; - - background-color: #edece6; - - margin-top: 4px; - padding: 4px 0px 2px 0px; - border: solid #d9d8d1; - border-width: 1px 0px 1px 0px; -} - -div.diff.header a.path { - text-decoration: underline; -} - -div.diff.extended_header, -div.diff.extended_header a.path, -div.diff.extended_header a.hash { - color: #777777; -} - -div.diff.extended_header .info { - color: #b0b0b0; -} - -div.diff.extended_header { - background-color: #f6f5ee; - padding: 2px 0px 2px 0px; -} - -div.diff a.list, -div.diff a.path, -div.diff a.hash { - text-decoration: none; -} - -div.diff a.list:hover, -div.diff a.path:hover, -div.diff a.hash:hover { - text-decoration: underline; -} - -div.diff.to_file a.path, -div.diff.to_file { - color: #007000; -} - -div.diff.add { - color: #008800; -} - -div.diff.from_file a.path, -div.diff.from_file { - color: #aa0000; -} - -div.diff.rem { - color: #cc0000; -} - -div.diff.chunk_header a, -div.diff.chunk_header { - color: #990099; -} - -div.diff.chunk_header { - border: dotted #ffe0ff; - border-width: 1px 0px 0px 0px; - margin-top: 2px; -} - -div.diff.chunk_header span.chunk_info { - background-color: #ffeeff; -} - -div.diff.chunk_header span.section { - color: #aa22aa; -} - -div.diff.incomplete { - color: #cccccc; -} - -div.diff.nodifferences { - font-weight: bold; - color: #600000; -} - -div.index_include { - border: solid #d9d8d1; - border-width: 0px 0px 1px; - padding: 12px 8px; -} - -div.search { - font-size: 100%; - font-weight: normal; - margin: 4px 8px; - float: right; - top: 56px; - right: 12px -} - -p.projsearch { - text-align: center; -} - -td.linenr { - text-align: right; -} - -a.linenr { - color: #999999; - text-decoration: none -} - -a.rss_logo { - float: right; - padding: 3px 0px; - width: 35px; - line-height: 10px; - border: 1px solid; - border-color: #fcc7a5 #7d3302 #3e1a01 #ff954e; - color: #ffffff; - background-color: #ff6600; - font-weight: bold; - font-family: sans-serif; - font-size: 70%; - text-align: center; - text-decoration: none; -} - -a.rss_logo:hover { - background-color: #ee5500; -} - -a.rss_logo.generic { - background-color: #ff8800; -} - -a.rss_logo.generic:hover { - background-color: #ee7700; -} - -span.refs span { - padding: 0px 4px; - font-size: 70%; - font-weight: normal; - border: 1px solid; - background-color: #ffaaff; - border-color: #ffccff #ff00ee #ff00ee #ffccff; -} - -span.refs span a { - text-decoration: none; - color: inherit; -} - -span.refs span a:hover { - text-decoration: underline; -} - -span.refs span.indirect { - font-style: italic; -} - -span.refs span.ref { - background-color: #aaaaff; - border-color: #ccccff #0033cc #0033cc #ccccff; -} - -span.refs span.tag { - background-color: #ffffaa; - border-color: #ffffcc #ffee00 #ffee00 #ffffcc; -} - -span.refs span.head { - background-color: #aaffaa; - border-color: #ccffcc #00cc33 #00cc33 #ccffcc; -} - -span.atnight { - color: #cc0000; -} - -span.match { - color: #e00000; -} - -div.binary { - font-style: italic; -} diff --git a/gitweb/gitweb.js b/gitweb/gitweb.js deleted file mode 100644 index 9c66928c4a..0000000000 --- a/gitweb/gitweb.js +++ /dev/null @@ -1,875 +0,0 @@ -// Copyright (C) 2007, Fredrik Kuivinen -// 2007, Petr Baudis -// 2008-2009, Jakub Narebski - -/** - * @fileOverview JavaScript code for gitweb (git web interface). - * @license GPLv2 or later - */ - -/* ============================================================ */ -/* functions for generic gitweb actions and views */ - -/** - * used to check if link has 'js' query parameter already (at end), - * and other reasons to not add 'js=1' param at the end of link - * @constant - */ -var jsExceptionsRe = /[;?]js=[01]$/; - -/** - * Add '?js=1' or ';js=1' to the end of every link in the document - * that doesn't have 'js' query parameter set already. - * - * Links with 'js=1' lead to JavaScript version of given action, if it - * exists (currently there is only 'blame_incremental' for 'blame') - * - * @globals jsExceptionsRe - */ -function fixLinks() { - var allLinks = document.getElementsByTagName("a") || document.links; - for (var i = 0, len = allLinks.length; i < len; i++) { - var link = allLinks[i]; - if (!jsExceptionsRe.test(link)) { // =~ /[;?]js=[01]$/; - link.href += - (link.href.indexOf('?') === -1 ? '?' : ';') + 'js=1'; - } - } -} - - -/* ============================================================ */ - -/* - * This code uses DOM methods instead of (nonstandard) innerHTML - * to modify page. - * - * innerHTML is non-standard IE extension, though supported by most - * browsers; however Firefox up to version 1.5 didn't implement it in - * a strict mode (application/xml+xhtml mimetype). - * - * Also my simple benchmarks show that using elem.firstChild.data = - * 'content' is slightly faster than elem.innerHTML = 'content'. It - * is however more fragile (text element fragment must exists), and - * less feature-rich (we cannot add HTML). - * - * Note that DOM 2 HTML is preferred over generic DOM 2 Core; the - * equivalent using DOM 2 Core is usually shown in comments. - */ - - -/* ============================================================ */ -/* generic utility functions */ - - -/** - * pad number N with nonbreakable spaces on the left, to WIDTH characters - * example: padLeftStr(12, 3, '\u00A0') == '\u00A012' - * ('\u00A0' is nonbreakable space) - * - * @param {Number|String} input: number to pad - * @param {Number} width: visible width of output - * @param {String} str: string to prefix to string, e.g. '\u00A0' - * @returns {String} INPUT prefixed with (WIDTH - INPUT.length) x STR - */ -function padLeftStr(input, width, str) { - var prefix = ''; - - width -= input.toString().length; - while (width > 0) { - prefix += str; - width--; - } - return prefix + input; -} - -/** - * Pad INPUT on the left to SIZE width, using given padding character CH, - * for example padLeft('a', 3, '_') is '__a'. - * - * @param {String} input: input value converted to string. - * @param {Number} width: desired length of output. - * @param {String} ch: single character to prefix to string. - * - * @returns {String} Modified string, at least SIZE length. - */ -function padLeft(input, width, ch) { - var s = input + ""; - while (s.length < width) { - s = ch + s; - } - return s; -} - -/** - * Create XMLHttpRequest object in cross-browser way - * @returns XMLHttpRequest object, or null - */ -function createRequestObject() { - try { - return new XMLHttpRequest(); - } catch (e) {} - try { - return window.createRequest(); - } catch (e) {} - try { - return new ActiveXObject("Msxml2.XMLHTTP"); - } catch (e) {} - try { - return new ActiveXObject("Microsoft.XMLHTTP"); - } catch (e) {} - - return null; -} - - -/* ============================================================ */ -/* utility/helper functions (and variables) */ - -var xhr; // XMLHttpRequest object -var projectUrl; // partial query + separator ('?' or ';') - -// 'commits' is an associative map. It maps SHA1s to Commit objects. -var commits = {}; - -/** - * constructor for Commit objects, used in 'blame' - * @class Represents a blamed commit - * @param {String} sha1: SHA-1 identifier of a commit - */ -function Commit(sha1) { - if (this instanceof Commit) { - this.sha1 = sha1; - this.nprevious = 0; /* number of 'previous', effective parents */ - } else { - return new Commit(sha1); - } -} - -/* ............................................................ */ -/* progress info, timing, error reporting */ - -var blamedLines = 0; -var totalLines = '???'; -var div_progress_bar; -var div_progress_info; - -/** - * Detects how many lines does a blamed file have, - * This information is used in progress info - * - * @returns {Number|String} Number of lines in file, or string '...' - */ -function countLines() { - var table = - document.getElementById('blame_table') || - document.getElementsByTagName('table')[0]; - - if (table) { - return table.getElementsByTagName('tr').length - 1; // for header - } else { - return '...'; - } -} - -/** - * update progress info and length (width) of progress bar - * - * @globals div_progress_info, div_progress_bar, blamedLines, totalLines - */ -function updateProgressInfo() { - if (!div_progress_info) { - div_progress_info = document.getElementById('progress_info'); - } - if (!div_progress_bar) { - div_progress_bar = document.getElementById('progress_bar'); - } - if (!div_progress_info && !div_progress_bar) { - return; - } - - var percentage = Math.floor(100.0*blamedLines/totalLines); - - if (div_progress_info) { - div_progress_info.firstChild.data = blamedLines + ' / ' + totalLines + - ' (' + padLeftStr(percentage, 3, '\u00A0') + '%)'; - } - - if (div_progress_bar) { - //div_progress_bar.setAttribute('style', 'width: '+percentage+'%;'); - div_progress_bar.style.width = percentage + '%'; - } -} - - -var t_interval_server = ''; -var cmds_server = ''; -var t0 = new Date(); - -/** - * write how much it took to generate data, and to run script - * - * @globals t0, t_interval_server, cmds_server - */ -function writeTimeInterval() { - var info_time = document.getElementById('generating_time'); - if (!info_time || !t_interval_server) { - return; - } - var t1 = new Date(); - info_time.firstChild.data += ' + (' + - t_interval_server + ' sec server blame_data / ' + - (t1.getTime() - t0.getTime())/1000 + ' sec client JavaScript)'; - - var info_cmds = document.getElementById('generating_cmd'); - if (!info_time || !cmds_server) { - return; - } - info_cmds.firstChild.data += ' + ' + cmds_server; -} - -/** - * show an error message alert to user within page (in prohress info area) - * @param {String} str: plain text error message (no HTML) - * - * @globals div_progress_info - */ -function errorInfo(str) { - if (!div_progress_info) { - div_progress_info = document.getElementById('progress_info'); - } - if (div_progress_info) { - div_progress_info.className = 'error'; - div_progress_info.firstChild.data = str; - } -} - -/* ............................................................ */ -/* coloring rows during blame_data (git blame --incremental) run */ - -/** - * used to extract N from 'colorN', where N is a number, - * @constant - */ -var colorRe = /\bcolor([0-9]*)\b/; - -/** - * return N if , otherwise return null - * (some browsers require CSS class names to begin with letter) - * - * @param {HTMLElement} tr: table row element to check - * @param {String} tr.className: 'class' attribute of tr element - * @returns {Number|null} N if tr.className == 'colorN', otherwise null - * - * @globals colorRe - */ -function getColorNo(tr) { - if (!tr) { - return null; - } - var className = tr.className; - if (className) { - var match = colorRe.exec(className); - if (match) { - return parseInt(match[1], 10); - } - } - return null; -} - -var colorsFreq = [0, 0, 0]; -/** - * return one of given possible colors (curently least used one) - * example: chooseColorNoFrom(2, 3) returns 2 or 3 - * - * @param {Number[]} arguments: one or more numbers - * assumes that 1 <= arguments[i] <= colorsFreq.length - * @returns {Number} Least used color number from arguments - * @globals colorsFreq - */ -function chooseColorNoFrom() { - // choose the color which is least used - var colorNo = arguments[0]; - for (var i = 1; i < arguments.length; i++) { - if (colorsFreq[arguments[i]-1] < colorsFreq[colorNo-1]) { - colorNo = arguments[i]; - } - } - colorsFreq[colorNo-1]++; - return colorNo; -} - -/** - * given two neigbour elements, find color which would be different - * from color of both of neighbours; used to 3-color blame table - * - * @param {HTMLElement} tr_prev - * @param {HTMLElement} tr_next - * @returns {Number} color number N such that - * colorN != tr_prev.className && colorN != tr_next.className - */ -function findColorNo(tr_prev, tr_next) { - var color_prev = getColorNo(tr_prev); - var color_next = getColorNo(tr_next); - - - // neither of neighbours has color set - // THEN we can use any of 3 possible colors - if (!color_prev && !color_next) { - return chooseColorNoFrom(1,2,3); - } - - // either both neighbours have the same color, - // or only one of neighbours have color set - // THEN we can use any color except given - var color; - if (color_prev === color_next) { - color = color_prev; // = color_next; - } else if (!color_prev) { - color = color_next; - } else if (!color_next) { - color = color_prev; - } - if (color) { - return chooseColorNoFrom((color % 3) + 1, ((color+1) % 3) + 1); - } - - // neighbours have different colors - // THEN there is only one color left - return (3 - ((color_prev + color_next) % 3)); -} - -/* ............................................................ */ -/* coloring rows like 'blame' after 'blame_data' finishes */ - -/** - * returns true if given row element (tr) is first in commit group - * to be used only after 'blame_data' finishes (after processing) - * - * @param {HTMLElement} tr: table row - * @returns {Boolean} true if TR is first in commit group - */ -function isStartOfGroup(tr) { - return tr.firstChild.className === 'sha1'; -} - -/** - * change colors to use zebra coloring (2 colors) instead of 3 colors - * concatenate neighbour commit groups belonging to the same commit - * - * @globals colorRe - */ -function fixColorsAndGroups() { - var colorClasses = ['light', 'dark']; - var linenum = 1; - var tr, prev_group; - var colorClass = 0; - var table = - document.getElementById('blame_table') || - document.getElementsByTagName('table')[0]; - - while ((tr = document.getElementById('l'+linenum))) { - // index origin is 0, which is table header; start from 1 - //while ((tr = table.rows[linenum])) { // <- it is slower - if (isStartOfGroup(tr, linenum, document)) { - if (prev_group && - prev_group.firstChild.firstChild.href === - tr.firstChild.firstChild.href) { - // we have to concatenate groups - var prev_rows = prev_group.firstChild.rowSpan || 1; - var curr_rows = tr.firstChild.rowSpan || 1; - prev_group.firstChild.rowSpan = prev_rows + curr_rows; - //tr.removeChild(tr.firstChild); - tr.deleteCell(0); // DOM2 HTML way - } else { - colorClass = (colorClass + 1) % 2; - prev_group = tr; - } - } - var tr_class = tr.className; - tr.className = tr_class.replace(colorRe, colorClasses[colorClass]); - linenum++; - } -} - -/* ............................................................ */ -/* time and data */ - -/** - * used to extract hours and minutes from timezone info, e.g '-0900' - * @constant - */ -var tzRe = /^([+-][0-9][0-9])([0-9][0-9])$/; - -/** - * return date in local time formatted in iso-8601 like format - * 'yyyy-mm-dd HH:MM:SS +/-ZZZZ' e.g. '2005-08-07 21:49:46 +0200' - * - * @param {Number} epoch: seconds since '00:00:00 1970-01-01 UTC' - * @param {String} timezoneInfo: numeric timezone '(+|-)HHMM' - * @returns {String} date in local time in iso-8601 like format - * - * @globals tzRe - */ -function formatDateISOLocal(epoch, timezoneInfo) { - var match = tzRe.exec(timezoneInfo); - // date corrected by timezone - var localDate = new Date(1000 * (epoch + - (parseInt(match[1],10)*3600 + parseInt(match[2],10)*60))); - var localDateStr = // e.g. '2005-08-07' - localDate.getUTCFullYear() + '-' + - padLeft(localDate.getUTCMonth()+1, 2, '0') + '-' + - padLeft(localDate.getUTCDate(), 2, '0'); - var localTimeStr = // e.g. '21:49:46' - padLeft(localDate.getUTCHours(), 2, '0') + ':' + - padLeft(localDate.getUTCMinutes(), 2, '0') + ':' + - padLeft(localDate.getUTCSeconds(), 2, '0'); - - return localDateStr + ' ' + localTimeStr + ' ' + timezoneInfo; -} - -/* ............................................................ */ -/* unquoting/unescaping filenames */ - -/**#@+ - * @constant - */ -var escCodeRe = /\\([^0-7]|[0-7]{1,3})/g; -var octEscRe = /^[0-7]{1,3}$/; -var maybeQuotedRe = /^\"(.*)\"$/; -/**#@-*/ - -/** - * unquote maybe git-quoted filename - * e.g. 'aa' -> 'aa', '"a\ta"' -> 'a a' - * - * @param {String} str: git-quoted string - * @returns {String} Unquoted and unescaped string - * - * @globals escCodeRe, octEscRe, maybeQuotedRe - */ -function unquote(str) { - function unq(seq) { - var es = { - // character escape codes, aka escape sequences (from C) - // replacements are to some extent JavaScript specific - t: "\t", // tab (HT, TAB) - n: "\n", // newline (NL) - r: "\r", // return (CR) - f: "\f", // form feed (FF) - b: "\b", // backspace (BS) - a: "\x07", // alarm (bell) (BEL) - e: "\x1B", // escape (ESC) - v: "\v" // vertical tab (VT) - }; - - if (seq.search(octEscRe) !== -1) { - // octal char sequence - return String.fromCharCode(parseInt(seq, 8)); - } else if (seq in es) { - // C escape sequence, aka character escape code - return es[seq]; - } - // quoted ordinary character - return seq; - } - - var match = str.match(maybeQuotedRe); - if (match) { - str = match[1]; - // perhaps str = eval('"'+str+'"'); would be enough? - str = str.replace(escCodeRe, - function (substr, p1, offset, s) { return unq(p1); }); - } - return str; -} - -/* ============================================================ */ -/* main part: parsing response */ - -/** - * Function called for each blame entry, as soon as it finishes. - * It updates page via DOM manipulation, adding sha1 info, etc. - * - * @param {Commit} commit: blamed commit - * @param {Object} group: object representing group of lines, - * which blame the same commit (blame entry) - * - * @globals blamedLines - */ -function handleLine(commit, group) { - /* - This is the structure of the HTML fragment we are working - with: - - - - 123 - # times (my ext3 doesn't). - - */ - - var resline = group.resline; - - // format date and time string only once per commit - if (!commit.info) { - /* e.g. 'Kay Sievers, 2005-08-07 21:49:46 +0200' */ - commit.info = commit.author + ', ' + - formatDateISOLocal(commit.authorTime, commit.authorTimezone); - } - - // color depends on group of lines, not only on blamed commit - var colorNo = findColorNo( - document.getElementById('l'+(resline-1)), - document.getElementById('l'+(resline+group.numlines)) - ); - - // loop over lines in commit group - for (var i = 0; i < group.numlines; i++, resline++) { - var tr = document.getElementById('l'+resline); - if (!tr) { - break; - } - /* - - - 123 - # times (my ext3 doesn't). - - */ - var td_sha1 = tr.firstChild; - var a_sha1 = td_sha1.firstChild; - var a_linenr = td_sha1.nextSibling.firstChild; - - /* */ - var tr_class = ''; - if (colorNo !== null) { - tr_class = 'color'+colorNo; - } - if (commit.boundary) { - tr_class += ' boundary'; - } - if (commit.nprevious === 0) { - tr_class += ' no-previous'; - } else if (commit.nprevious > 1) { - tr_class += ' multiple-previous'; - } - tr.className = tr_class; - - /* ? */ - if (i === 0) { - td_sha1.title = commit.info; - td_sha1.rowSpan = group.numlines; - - a_sha1.href = projectUrl + 'a=commit;h=' + commit.sha1; - if (a_sha1.firstChild) { - a_sha1.firstChild.data = commit.sha1.substr(0, 8); - } else { - a_sha1.appendChild( - document.createTextNode(commit.sha1.substr(0, 8))); - } - if (group.numlines >= 2) { - var fragment = document.createDocumentFragment(); - var br = document.createElement("br"); - var match = commit.author.match(/\b([A-Z])\B/g); - if (match) { - var text = document.createTextNode( - match.join('')); - } - if (br && text) { - var elem = fragment || td_sha1; - elem.appendChild(br); - elem.appendChild(text); - if (fragment) { - td_sha1.appendChild(fragment); - } - } - } - } else { - //tr.removeChild(td_sha1); // DOM2 Core way - tr.deleteCell(0); // DOM2 HTML way - } - - /* 123 */ - var linenr_commit = - ('previous' in commit ? commit.previous : commit.sha1); - var linenr_filename = - ('file_parent' in commit ? commit.file_parent : commit.filename); - a_linenr.href = projectUrl + 'a=blame_incremental' + - ';hb=' + linenr_commit + - ';f=' + encodeURIComponent(linenr_filename) + - '#l' + (group.srcline + i); - - blamedLines++; - - //updateProgressInfo(); - } -} - -// ---------------------------------------------------------------------- - -var inProgress = false; // are we processing response - -/**#@+ - * @constant - */ -var sha1Re = /^([0-9a-f]{40}) ([0-9]+) ([0-9]+) ([0-9]+)/; -var infoRe = /^([a-z-]+) ?(.*)/; -var endRe = /^END ?([^ ]*) ?(.*)/; -/**@-*/ - -var curCommit = new Commit(); -var curGroup = {}; - -var pollTimer = null; - -/** - * Parse output from 'git blame --incremental [...]', received via - * XMLHttpRequest from server (blamedataUrl), and call handleLine - * (which updates page) as soon as blame entry is completed. - * - * @param {String[]} lines: new complete lines from blamedata server - * - * @globals commits, curCommit, curGroup, t_interval_server, cmds_server - * @globals sha1Re, infoRe, endRe - */ -function processBlameLines(lines) { - var match; - - for (var i = 0, len = lines.length; i < len; i++) { - - if ((match = sha1Re.exec(lines[i]))) { - var sha1 = match[1]; - var srcline = parseInt(match[2], 10); - var resline = parseInt(match[3], 10); - var numlines = parseInt(match[4], 10); - - var c = commits[sha1]; - if (!c) { - c = new Commit(sha1); - commits[sha1] = c; - } - curCommit = c; - - curGroup.srcline = srcline; - curGroup.resline = resline; - curGroup.numlines = numlines; - - } else if ((match = infoRe.exec(lines[i]))) { - var info = match[1]; - var data = match[2]; - switch (info) { - case 'filename': - curCommit.filename = unquote(data); - // 'filename' information terminates the entry - handleLine(curCommit, curGroup); - updateProgressInfo(); - break; - case 'author': - curCommit.author = data; - break; - case 'author-time': - curCommit.authorTime = parseInt(data, 10); - break; - case 'author-tz': - curCommit.authorTimezone = data; - break; - case 'previous': - curCommit.nprevious++; - // store only first 'previous' header - if (!'previous' in curCommit) { - var parts = data.split(' ', 2); - curCommit.previous = parts[0]; - curCommit.file_parent = unquote(parts[1]); - } - break; - case 'boundary': - curCommit.boundary = true; - break; - } // end switch - - } else if ((match = endRe.exec(lines[i]))) { - t_interval_server = match[1]; - cmds_server = match[2]; - - } else if (lines[i] !== '') { - // malformed line - - } // end if (match) - - } // end for (lines) -} - -/** - * Process new data and return pointer to end of processed part - * - * @param {String} unprocessed: new data (from nextReadPos) - * @param {Number} nextReadPos: end of last processed data - * @return {Number} end of processed data (new value for nextReadPos) - */ -function processData(unprocessed, nextReadPos) { - var lastLineEnd = unprocessed.lastIndexOf('\n'); - if (lastLineEnd !== -1) { - var lines = unprocessed.substring(0, lastLineEnd).split('\n'); - nextReadPos += lastLineEnd + 1 /* 1 == '\n'.length */; - - processBlameLines(lines); - } // end if - - return nextReadPos; -} - -/** - * Handle XMLHttpRequest errors - * - * @param {XMLHttpRequest} xhr: XMLHttpRequest object - * - * @globals pollTimer, commits, inProgress - */ -function handleError(xhr) { - errorInfo('Server error: ' + - xhr.status + ' - ' + (xhr.statusText || 'Error contacting server')); - - clearInterval(pollTimer); - commits = {}; // free memory - - inProgress = false; -} - -/** - * Called after XMLHttpRequest finishes (loads) - * - * @param {XMLHttpRequest} xhr: XMLHttpRequest object (unused) - * - * @globals pollTimer, commits, inProgress - */ -function responseLoaded(xhr) { - clearInterval(pollTimer); - - fixColorsAndGroups(); - writeTimeInterval(); - commits = {}; // free memory - - inProgress = false; -} - -/** - * handler for XMLHttpRequest onreadystatechange event - * @see startBlame - * - * @globals xhr, inProgress - */ -function handleResponse() { - - /* - * xhr.readyState - * - * Value Constant (W3C) Description - * ------------------------------------------------------------------- - * 0 UNSENT open() has not been called yet. - * 1 OPENED send() has not been called yet. - * 2 HEADERS_RECEIVED send() has been called, and headers - * and status are available. - * 3 LOADING Downloading; responseText holds partial data. - * 4 DONE The operation is complete. - */ - - if (xhr.readyState !== 4 && xhr.readyState !== 3) { - return; - } - - // the server returned error - // try ... catch block is to work around bug in IE8 - try { - if (xhr.readyState === 3 && xhr.status !== 200) { - return; - } - } catch (e) { - return; - } - if (xhr.readyState === 4 && xhr.status !== 200) { - handleError(xhr); - return; - } - - // In konqueror xhr.responseText is sometimes null here... - if (xhr.responseText === null) { - return; - } - - // in case we were called before finished processing - if (inProgress) { - return; - } else { - inProgress = true; - } - - // extract new whole (complete) lines, and process them - while (xhr.prevDataLength !== xhr.responseText.length) { - if (xhr.readyState === 4 && - xhr.prevDataLength === xhr.responseText.length) { - break; - } - - xhr.prevDataLength = xhr.responseText.length; - var unprocessed = xhr.responseText.substring(xhr.nextReadPos); - xhr.nextReadPos = processData(unprocessed, xhr.nextReadPos); - } // end while - - // did we finish work? - if (xhr.readyState === 4 && - xhr.prevDataLength === xhr.responseText.length) { - responseLoaded(xhr); - } - - inProgress = false; -} - -// ============================================================ -// ------------------------------------------------------------ - -/** - * Incrementally update line data in blame_incremental view in gitweb. - * - * @param {String} blamedataUrl: URL to server script generating blame data. - * @param {String} bUrl: partial URL to project, used to generate links. - * - * Called from 'blame_incremental' view after loading table with - * file contents, a base for blame view. - * - * @globals xhr, t0, projectUrl, div_progress_bar, totalLines, pollTimer -*/ -function startBlame(blamedataUrl, bUrl) { - - xhr = createRequestObject(); - if (!xhr) { - errorInfo('ERROR: XMLHttpRequest not supported'); - return; - } - - t0 = new Date(); - projectUrl = bUrl + (bUrl.indexOf('?') === -1 ? '?' : ';'); - if ((div_progress_bar = document.getElementById('progress_bar'))) { - //div_progress_bar.setAttribute('style', 'width: 100%;'); - div_progress_bar.style.cssText = 'width: 100%;'; - } - totalLines = countLines(); - updateProgressInfo(); - - /* add extra properties to xhr object to help processing response */ - xhr.prevDataLength = -1; // used to detect if we have new data - xhr.nextReadPos = 0; // where unread part of response starts - - xhr.onreadystatechange = handleResponse; - //xhr.onreadystatechange = function () { handleResponse(xhr); }; - - xhr.open('GET', blamedataUrl); - xhr.setRequestHeader('Accept', 'text/plain'); - xhr.send(null); - - // not all browsers call onreadystatechange event on each server flush - // poll response using timer every second to handle this issue - pollTimer = setInterval(xhr.onreadystatechange, 1000); -} - -// end of gitweb.js diff --git a/gitweb/static/git-favicon.png b/gitweb/static/git-favicon.png new file mode 100644 index 0000000000..aae35a70e7 Binary files /dev/null and b/gitweb/static/git-favicon.png differ diff --git a/gitweb/static/git-logo.png b/gitweb/static/git-logo.png new file mode 100644 index 0000000000..f4ede2e944 Binary files /dev/null and b/gitweb/static/git-logo.png differ diff --git a/gitweb/static/gitweb.css b/gitweb/static/gitweb.css new file mode 100644 index 0000000000..50067f2e0d --- /dev/null +++ b/gitweb/static/gitweb.css @@ -0,0 +1,574 @@ +body { + font-family: sans-serif; + font-size: small; + border: solid #d9d8d1; + border-width: 1px; + margin: 10px; + background-color: #ffffff; + color: #000000; +} + +a { + color: #0000cc; +} + +a:hover, a:visited, a:active { + color: #880000; +} + +span.cntrl { + border: dashed #aaaaaa; + border-width: 1px; + padding: 0px 2px 0px 2px; + margin: 0px 2px 0px 2px; +} + +img.logo { + float: right; + border-width: 0px; +} + +img.avatar { + vertical-align: middle; +} + +a.list img.avatar { + border-style: none; +} + +div.page_header { + height: 25px; + padding: 8px; + font-size: 150%; + font-weight: bold; + background-color: #d9d8d1; +} + +div.page_header a:visited, a.header { + color: #0000cc; +} + +div.page_header a:hover { + color: #880000; +} + +div.page_nav { + padding: 8px; +} + +div.page_nav a:visited { + color: #0000cc; +} + +div.page_path { + padding: 8px; + font-weight: bold; + border: solid #d9d8d1; + border-width: 0px 0px 1px; +} + +div.page_footer { + height: 17px; + padding: 4px 8px; + background-color: #d9d8d1; +} + +div.page_footer_text { + float: left; + color: #555555; + font-style: italic; +} + +div#generating_info { + margin: 4px; + font-size: smaller; + text-align: center; + color: #505050; +} + +div.page_body { + padding: 8px; + font-family: monospace; +} + +div.title, a.title { + display: block; + padding: 6px 8px; + font-weight: bold; + background-color: #edece6; + text-decoration: none; + color: #000000; +} + +div.readme { + padding: 8px; +} + +a.title:hover { + background-color: #d9d8d1; +} + +div.title_text { + padding: 6px 0px; + border: solid #d9d8d1; + border-width: 0px 0px 1px; + font-family: monospace; +} + +div.log_body { + padding: 8px 8px 8px 150px; +} + +span.age { + position: relative; + float: left; + width: 142px; + font-style: italic; +} + +span.signoff { + color: #888888; +} + +div.log_link { + padding: 0px 8px; + font-size: 70%; + font-family: sans-serif; + font-style: normal; + position: relative; + float: left; + width: 136px; +} + +div.list_head { + padding: 6px 8px 4px; + border: solid #d9d8d1; + border-width: 1px 0px 0px; + font-style: italic; +} + +.author_date, .author { + font-style: italic; +} + +div.author_date { + padding: 8px; + border: solid #d9d8d1; + border-width: 0px 0px 1px 0px; +} + +a.list { + text-decoration: none; + color: #000000; +} + +a.subject, a.name { + font-weight: bold; +} + +table.tags a.subject { + font-weight: normal; +} + +a.list:hover { + text-decoration: underline; + color: #880000; +} + +a.text { + text-decoration: none; + color: #0000cc; +} + +a.text:visited { + text-decoration: none; + color: #880000; +} + +a.text:hover { + text-decoration: underline; + color: #880000; +} + +table { + padding: 8px 4px; + border-spacing: 0; +} + +table.diff_tree { + font-family: monospace; +} + +table.combined.diff_tree th { + text-align: center; +} + +table.combined.diff_tree td { + padding-right: 24px; +} + +table.combined.diff_tree th.link, +table.combined.diff_tree td.link { + padding: 0px 2px; +} + +table.combined.diff_tree td.nochange a { + color: #6666ff; +} + +table.combined.diff_tree td.nochange a:hover, +table.combined.diff_tree td.nochange a:visited { + color: #d06666; +} + +table.blame { + border-collapse: collapse; +} + +table.blame td { + padding: 0px 5px; + font-size: 100%; + vertical-align: top; +} + +th { + padding: 2px 5px; + font-size: 100%; + text-align: left; +} + +/* do not change row style on hover for 'blame' view */ +tr.light, +table.blame .light:hover { + background-color: #ffffff; +} + +tr.dark, +table.blame .dark:hover { + background-color: #f6f6f0; +} + +/* currently both use the same, but it can change */ +tr.light:hover, +tr.dark:hover { + background-color: #edece6; +} + +/* boundary commits in 'blame' view */ +/* and commits without "previous" */ +tr.boundary td.sha1, +tr.no-previous td.linenr { + font-weight: bold; +} + +/* for 'blame_incremental', during processing */ +tr.color1 { background-color: #f6fff6; } +tr.color2 { background-color: #f6f6ff; } +tr.color3 { background-color: #fff6f6; } + +td { + padding: 2px 5px; + font-size: 100%; + vertical-align: top; +} + +td.link, td.selflink { + padding: 2px 5px; + font-family: sans-serif; + font-size: 70%; +} + +td.selflink { + padding-right: 0px; +} + +td.sha1 { + font-family: monospace; +} + +.error { + color: red; + background-color: yellow; +} + +td.current_head { + text-decoration: underline; +} + +table.diff_tree span.file_status.new { + color: #008000; +} + +table.diff_tree span.file_status.deleted { + color: #c00000; +} + +table.diff_tree span.file_status.moved, +table.diff_tree span.file_status.mode_chnge { + color: #777777; +} + +table.diff_tree span.file_status.copied { + color: #70a070; +} + +/* noage: "No commits" */ +table.project_list td.noage { + color: #808080; + font-style: italic; +} + +/* age2: 60*60*24*2 <= age */ +table.project_list td.age2, table.blame td.age2 { + font-style: italic; +} + +/* age1: 60*60*2 <= age < 60*60*24*2 */ +table.project_list td.age1 { + color: #009900; + font-style: italic; +} + +table.blame td.age1 { + color: #009900; + background: transparent; +} + +/* age0: age < 60*60*2 */ +table.project_list td.age0 { + color: #009900; + font-style: italic; + font-weight: bold; +} + +table.blame td.age0 { + color: #009900; + background: transparent; + font-weight: bold; +} + +td.pre, div.pre, div.diff { + font-family: monospace; + font-size: 12px; + white-space: pre; +} + +td.mode { + font-family: monospace; +} + +/* progress of blame_interactive */ +div#progress_bar { + height: 2px; + margin-bottom: -2px; + background-color: #d8d9d0; +} +div#progress_info { + float: right; + text-align: right; +} + +/* format of (optional) objects size in 'tree' view */ +td.size { + font-family: monospace; + text-align: right; +} + +/* styling of diffs (patchsets): commitdiff and blobdiff views */ +div.diff.header, +div.diff.extended_header { + white-space: normal; +} + +div.diff.header { + font-weight: bold; + + background-color: #edece6; + + margin-top: 4px; + padding: 4px 0px 2px 0px; + border: solid #d9d8d1; + border-width: 1px 0px 1px 0px; +} + +div.diff.header a.path { + text-decoration: underline; +} + +div.diff.extended_header, +div.diff.extended_header a.path, +div.diff.extended_header a.hash { + color: #777777; +} + +div.diff.extended_header .info { + color: #b0b0b0; +} + +div.diff.extended_header { + background-color: #f6f5ee; + padding: 2px 0px 2px 0px; +} + +div.diff a.list, +div.diff a.path, +div.diff a.hash { + text-decoration: none; +} + +div.diff a.list:hover, +div.diff a.path:hover, +div.diff a.hash:hover { + text-decoration: underline; +} + +div.diff.to_file a.path, +div.diff.to_file { + color: #007000; +} + +div.diff.add { + color: #008800; +} + +div.diff.from_file a.path, +div.diff.from_file { + color: #aa0000; +} + +div.diff.rem { + color: #cc0000; +} + +div.diff.chunk_header a, +div.diff.chunk_header { + color: #990099; +} + +div.diff.chunk_header { + border: dotted #ffe0ff; + border-width: 1px 0px 0px 0px; + margin-top: 2px; +} + +div.diff.chunk_header span.chunk_info { + background-color: #ffeeff; +} + +div.diff.chunk_header span.section { + color: #aa22aa; +} + +div.diff.incomplete { + color: #cccccc; +} + +div.diff.nodifferences { + font-weight: bold; + color: #600000; +} + +div.index_include { + border: solid #d9d8d1; + border-width: 0px 0px 1px; + padding: 12px 8px; +} + +div.search { + font-size: 100%; + font-weight: normal; + margin: 4px 8px; + float: right; + top: 56px; + right: 12px +} + +p.projsearch { + text-align: center; +} + +td.linenr { + text-align: right; +} + +a.linenr { + color: #999999; + text-decoration: none +} + +a.rss_logo { + float: right; + padding: 3px 0px; + width: 35px; + line-height: 10px; + border: 1px solid; + border-color: #fcc7a5 #7d3302 #3e1a01 #ff954e; + color: #ffffff; + background-color: #ff6600; + font-weight: bold; + font-family: sans-serif; + font-size: 70%; + text-align: center; + text-decoration: none; +} + +a.rss_logo:hover { + background-color: #ee5500; +} + +a.rss_logo.generic { + background-color: #ff8800; +} + +a.rss_logo.generic:hover { + background-color: #ee7700; +} + +span.refs span { + padding: 0px 4px; + font-size: 70%; + font-weight: normal; + border: 1px solid; + background-color: #ffaaff; + border-color: #ffccff #ff00ee #ff00ee #ffccff; +} + +span.refs span a { + text-decoration: none; + color: inherit; +} + +span.refs span a:hover { + text-decoration: underline; +} + +span.refs span.indirect { + font-style: italic; +} + +span.refs span.ref { + background-color: #aaaaff; + border-color: #ccccff #0033cc #0033cc #ccccff; +} + +span.refs span.tag { + background-color: #ffffaa; + border-color: #ffffcc #ffee00 #ffee00 #ffffcc; +} + +span.refs span.head { + background-color: #aaffaa; + border-color: #ccffcc #00cc33 #00cc33 #ccffcc; +} + +span.atnight { + color: #cc0000; +} + +span.match { + color: #e00000; +} + +div.binary { + font-style: italic; +} diff --git a/gitweb/static/gitweb.js b/gitweb/static/gitweb.js new file mode 100644 index 0000000000..9c66928c4a --- /dev/null +++ b/gitweb/static/gitweb.js @@ -0,0 +1,875 @@ +// Copyright (C) 2007, Fredrik Kuivinen +// 2007, Petr Baudis +// 2008-2009, Jakub Narebski + +/** + * @fileOverview JavaScript code for gitweb (git web interface). + * @license GPLv2 or later + */ + +/* ============================================================ */ +/* functions for generic gitweb actions and views */ + +/** + * used to check if link has 'js' query parameter already (at end), + * and other reasons to not add 'js=1' param at the end of link + * @constant + */ +var jsExceptionsRe = /[;?]js=[01]$/; + +/** + * Add '?js=1' or ';js=1' to the end of every link in the document + * that doesn't have 'js' query parameter set already. + * + * Links with 'js=1' lead to JavaScript version of given action, if it + * exists (currently there is only 'blame_incremental' for 'blame') + * + * @globals jsExceptionsRe + */ +function fixLinks() { + var allLinks = document.getElementsByTagName("a") || document.links; + for (var i = 0, len = allLinks.length; i < len; i++) { + var link = allLinks[i]; + if (!jsExceptionsRe.test(link)) { // =~ /[;?]js=[01]$/; + link.href += + (link.href.indexOf('?') === -1 ? '?' : ';') + 'js=1'; + } + } +} + + +/* ============================================================ */ + +/* + * This code uses DOM methods instead of (nonstandard) innerHTML + * to modify page. + * + * innerHTML is non-standard IE extension, though supported by most + * browsers; however Firefox up to version 1.5 didn't implement it in + * a strict mode (application/xml+xhtml mimetype). + * + * Also my simple benchmarks show that using elem.firstChild.data = + * 'content' is slightly faster than elem.innerHTML = 'content'. It + * is however more fragile (text element fragment must exists), and + * less feature-rich (we cannot add HTML). + * + * Note that DOM 2 HTML is preferred over generic DOM 2 Core; the + * equivalent using DOM 2 Core is usually shown in comments. + */ + + +/* ============================================================ */ +/* generic utility functions */ + + +/** + * pad number N with nonbreakable spaces on the left, to WIDTH characters + * example: padLeftStr(12, 3, '\u00A0') == '\u00A012' + * ('\u00A0' is nonbreakable space) + * + * @param {Number|String} input: number to pad + * @param {Number} width: visible width of output + * @param {String} str: string to prefix to string, e.g. '\u00A0' + * @returns {String} INPUT prefixed with (WIDTH - INPUT.length) x STR + */ +function padLeftStr(input, width, str) { + var prefix = ''; + + width -= input.toString().length; + while (width > 0) { + prefix += str; + width--; + } + return prefix + input; +} + +/** + * Pad INPUT on the left to SIZE width, using given padding character CH, + * for example padLeft('a', 3, '_') is '__a'. + * + * @param {String} input: input value converted to string. + * @param {Number} width: desired length of output. + * @param {String} ch: single character to prefix to string. + * + * @returns {String} Modified string, at least SIZE length. + */ +function padLeft(input, width, ch) { + var s = input + ""; + while (s.length < width) { + s = ch + s; + } + return s; +} + +/** + * Create XMLHttpRequest object in cross-browser way + * @returns XMLHttpRequest object, or null + */ +function createRequestObject() { + try { + return new XMLHttpRequest(); + } catch (e) {} + try { + return window.createRequest(); + } catch (e) {} + try { + return new ActiveXObject("Msxml2.XMLHTTP"); + } catch (e) {} + try { + return new ActiveXObject("Microsoft.XMLHTTP"); + } catch (e) {} + + return null; +} + + +/* ============================================================ */ +/* utility/helper functions (and variables) */ + +var xhr; // XMLHttpRequest object +var projectUrl; // partial query + separator ('?' or ';') + +// 'commits' is an associative map. It maps SHA1s to Commit objects. +var commits = {}; + +/** + * constructor for Commit objects, used in 'blame' + * @class Represents a blamed commit + * @param {String} sha1: SHA-1 identifier of a commit + */ +function Commit(sha1) { + if (this instanceof Commit) { + this.sha1 = sha1; + this.nprevious = 0; /* number of 'previous', effective parents */ + } else { + return new Commit(sha1); + } +} + +/* ............................................................ */ +/* progress info, timing, error reporting */ + +var blamedLines = 0; +var totalLines = '???'; +var div_progress_bar; +var div_progress_info; + +/** + * Detects how many lines does a blamed file have, + * This information is used in progress info + * + * @returns {Number|String} Number of lines in file, or string '...' + */ +function countLines() { + var table = + document.getElementById('blame_table') || + document.getElementsByTagName('table')[0]; + + if (table) { + return table.getElementsByTagName('tr').length - 1; // for header + } else { + return '...'; + } +} + +/** + * update progress info and length (width) of progress bar + * + * @globals div_progress_info, div_progress_bar, blamedLines, totalLines + */ +function updateProgressInfo() { + if (!div_progress_info) { + div_progress_info = document.getElementById('progress_info'); + } + if (!div_progress_bar) { + div_progress_bar = document.getElementById('progress_bar'); + } + if (!div_progress_info && !div_progress_bar) { + return; + } + + var percentage = Math.floor(100.0*blamedLines/totalLines); + + if (div_progress_info) { + div_progress_info.firstChild.data = blamedLines + ' / ' + totalLines + + ' (' + padLeftStr(percentage, 3, '\u00A0') + '%)'; + } + + if (div_progress_bar) { + //div_progress_bar.setAttribute('style', 'width: '+percentage+'%;'); + div_progress_bar.style.width = percentage + '%'; + } +} + + +var t_interval_server = ''; +var cmds_server = ''; +var t0 = new Date(); + +/** + * write how much it took to generate data, and to run script + * + * @globals t0, t_interval_server, cmds_server + */ +function writeTimeInterval() { + var info_time = document.getElementById('generating_time'); + if (!info_time || !t_interval_server) { + return; + } + var t1 = new Date(); + info_time.firstChild.data += ' + (' + + t_interval_server + ' sec server blame_data / ' + + (t1.getTime() - t0.getTime())/1000 + ' sec client JavaScript)'; + + var info_cmds = document.getElementById('generating_cmd'); + if (!info_time || !cmds_server) { + return; + } + info_cmds.firstChild.data += ' + ' + cmds_server; +} + +/** + * show an error message alert to user within page (in prohress info area) + * @param {String} str: plain text error message (no HTML) + * + * @globals div_progress_info + */ +function errorInfo(str) { + if (!div_progress_info) { + div_progress_info = document.getElementById('progress_info'); + } + if (div_progress_info) { + div_progress_info.className = 'error'; + div_progress_info.firstChild.data = str; + } +} + +/* ............................................................ */ +/* coloring rows during blame_data (git blame --incremental) run */ + +/** + * used to extract N from 'colorN', where N is a number, + * @constant + */ +var colorRe = /\bcolor([0-9]*)\b/; + +/** + * return N if , otherwise return null + * (some browsers require CSS class names to begin with letter) + * + * @param {HTMLElement} tr: table row element to check + * @param {String} tr.className: 'class' attribute of tr element + * @returns {Number|null} N if tr.className == 'colorN', otherwise null + * + * @globals colorRe + */ +function getColorNo(tr) { + if (!tr) { + return null; + } + var className = tr.className; + if (className) { + var match = colorRe.exec(className); + if (match) { + return parseInt(match[1], 10); + } + } + return null; +} + +var colorsFreq = [0, 0, 0]; +/** + * return one of given possible colors (curently least used one) + * example: chooseColorNoFrom(2, 3) returns 2 or 3 + * + * @param {Number[]} arguments: one or more numbers + * assumes that 1 <= arguments[i] <= colorsFreq.length + * @returns {Number} Least used color number from arguments + * @globals colorsFreq + */ +function chooseColorNoFrom() { + // choose the color which is least used + var colorNo = arguments[0]; + for (var i = 1; i < arguments.length; i++) { + if (colorsFreq[arguments[i]-1] < colorsFreq[colorNo-1]) { + colorNo = arguments[i]; + } + } + colorsFreq[colorNo-1]++; + return colorNo; +} + +/** + * given two neigbour elements, find color which would be different + * from color of both of neighbours; used to 3-color blame table + * + * @param {HTMLElement} tr_prev + * @param {HTMLElement} tr_next + * @returns {Number} color number N such that + * colorN != tr_prev.className && colorN != tr_next.className + */ +function findColorNo(tr_prev, tr_next) { + var color_prev = getColorNo(tr_prev); + var color_next = getColorNo(tr_next); + + + // neither of neighbours has color set + // THEN we can use any of 3 possible colors + if (!color_prev && !color_next) { + return chooseColorNoFrom(1,2,3); + } + + // either both neighbours have the same color, + // or only one of neighbours have color set + // THEN we can use any color except given + var color; + if (color_prev === color_next) { + color = color_prev; // = color_next; + } else if (!color_prev) { + color = color_next; + } else if (!color_next) { + color = color_prev; + } + if (color) { + return chooseColorNoFrom((color % 3) + 1, ((color+1) % 3) + 1); + } + + // neighbours have different colors + // THEN there is only one color left + return (3 - ((color_prev + color_next) % 3)); +} + +/* ............................................................ */ +/* coloring rows like 'blame' after 'blame_data' finishes */ + +/** + * returns true if given row element (tr) is first in commit group + * to be used only after 'blame_data' finishes (after processing) + * + * @param {HTMLElement} tr: table row + * @returns {Boolean} true if TR is first in commit group + */ +function isStartOfGroup(tr) { + return tr.firstChild.className === 'sha1'; +} + +/** + * change colors to use zebra coloring (2 colors) instead of 3 colors + * concatenate neighbour commit groups belonging to the same commit + * + * @globals colorRe + */ +function fixColorsAndGroups() { + var colorClasses = ['light', 'dark']; + var linenum = 1; + var tr, prev_group; + var colorClass = 0; + var table = + document.getElementById('blame_table') || + document.getElementsByTagName('table')[0]; + + while ((tr = document.getElementById('l'+linenum))) { + // index origin is 0, which is table header; start from 1 + //while ((tr = table.rows[linenum])) { // <- it is slower + if (isStartOfGroup(tr, linenum, document)) { + if (prev_group && + prev_group.firstChild.firstChild.href === + tr.firstChild.firstChild.href) { + // we have to concatenate groups + var prev_rows = prev_group.firstChild.rowSpan || 1; + var curr_rows = tr.firstChild.rowSpan || 1; + prev_group.firstChild.rowSpan = prev_rows + curr_rows; + //tr.removeChild(tr.firstChild); + tr.deleteCell(0); // DOM2 HTML way + } else { + colorClass = (colorClass + 1) % 2; + prev_group = tr; + } + } + var tr_class = tr.className; + tr.className = tr_class.replace(colorRe, colorClasses[colorClass]); + linenum++; + } +} + +/* ............................................................ */ +/* time and data */ + +/** + * used to extract hours and minutes from timezone info, e.g '-0900' + * @constant + */ +var tzRe = /^([+-][0-9][0-9])([0-9][0-9])$/; + +/** + * return date in local time formatted in iso-8601 like format + * 'yyyy-mm-dd HH:MM:SS +/-ZZZZ' e.g. '2005-08-07 21:49:46 +0200' + * + * @param {Number} epoch: seconds since '00:00:00 1970-01-01 UTC' + * @param {String} timezoneInfo: numeric timezone '(+|-)HHMM' + * @returns {String} date in local time in iso-8601 like format + * + * @globals tzRe + */ +function formatDateISOLocal(epoch, timezoneInfo) { + var match = tzRe.exec(timezoneInfo); + // date corrected by timezone + var localDate = new Date(1000 * (epoch + + (parseInt(match[1],10)*3600 + parseInt(match[2],10)*60))); + var localDateStr = // e.g. '2005-08-07' + localDate.getUTCFullYear() + '-' + + padLeft(localDate.getUTCMonth()+1, 2, '0') + '-' + + padLeft(localDate.getUTCDate(), 2, '0'); + var localTimeStr = // e.g. '21:49:46' + padLeft(localDate.getUTCHours(), 2, '0') + ':' + + padLeft(localDate.getUTCMinutes(), 2, '0') + ':' + + padLeft(localDate.getUTCSeconds(), 2, '0'); + + return localDateStr + ' ' + localTimeStr + ' ' + timezoneInfo; +} + +/* ............................................................ */ +/* unquoting/unescaping filenames */ + +/**#@+ + * @constant + */ +var escCodeRe = /\\([^0-7]|[0-7]{1,3})/g; +var octEscRe = /^[0-7]{1,3}$/; +var maybeQuotedRe = /^\"(.*)\"$/; +/**#@-*/ + +/** + * unquote maybe git-quoted filename + * e.g. 'aa' -> 'aa', '"a\ta"' -> 'a a' + * + * @param {String} str: git-quoted string + * @returns {String} Unquoted and unescaped string + * + * @globals escCodeRe, octEscRe, maybeQuotedRe + */ +function unquote(str) { + function unq(seq) { + var es = { + // character escape codes, aka escape sequences (from C) + // replacements are to some extent JavaScript specific + t: "\t", // tab (HT, TAB) + n: "\n", // newline (NL) + r: "\r", // return (CR) + f: "\f", // form feed (FF) + b: "\b", // backspace (BS) + a: "\x07", // alarm (bell) (BEL) + e: "\x1B", // escape (ESC) + v: "\v" // vertical tab (VT) + }; + + if (seq.search(octEscRe) !== -1) { + // octal char sequence + return String.fromCharCode(parseInt(seq, 8)); + } else if (seq in es) { + // C escape sequence, aka character escape code + return es[seq]; + } + // quoted ordinary character + return seq; + } + + var match = str.match(maybeQuotedRe); + if (match) { + str = match[1]; + // perhaps str = eval('"'+str+'"'); would be enough? + str = str.replace(escCodeRe, + function (substr, p1, offset, s) { return unq(p1); }); + } + return str; +} + +/* ============================================================ */ +/* main part: parsing response */ + +/** + * Function called for each blame entry, as soon as it finishes. + * It updates page via DOM manipulation, adding sha1 info, etc. + * + * @param {Commit} commit: blamed commit + * @param {Object} group: object representing group of lines, + * which blame the same commit (blame entry) + * + * @globals blamedLines + */ +function handleLine(commit, group) { + /* + This is the structure of the HTML fragment we are working + with: + + + + 123 + # times (my ext3 doesn't). + + */ + + var resline = group.resline; + + // format date and time string only once per commit + if (!commit.info) { + /* e.g. 'Kay Sievers, 2005-08-07 21:49:46 +0200' */ + commit.info = commit.author + ', ' + + formatDateISOLocal(commit.authorTime, commit.authorTimezone); + } + + // color depends on group of lines, not only on blamed commit + var colorNo = findColorNo( + document.getElementById('l'+(resline-1)), + document.getElementById('l'+(resline+group.numlines)) + ); + + // loop over lines in commit group + for (var i = 0; i < group.numlines; i++, resline++) { + var tr = document.getElementById('l'+resline); + if (!tr) { + break; + } + /* + + + 123 + # times (my ext3 doesn't). + + */ + var td_sha1 = tr.firstChild; + var a_sha1 = td_sha1.firstChild; + var a_linenr = td_sha1.nextSibling.firstChild; + + /* */ + var tr_class = ''; + if (colorNo !== null) { + tr_class = 'color'+colorNo; + } + if (commit.boundary) { + tr_class += ' boundary'; + } + if (commit.nprevious === 0) { + tr_class += ' no-previous'; + } else if (commit.nprevious > 1) { + tr_class += ' multiple-previous'; + } + tr.className = tr_class; + + /* ? */ + if (i === 0) { + td_sha1.title = commit.info; + td_sha1.rowSpan = group.numlines; + + a_sha1.href = projectUrl + 'a=commit;h=' + commit.sha1; + if (a_sha1.firstChild) { + a_sha1.firstChild.data = commit.sha1.substr(0, 8); + } else { + a_sha1.appendChild( + document.createTextNode(commit.sha1.substr(0, 8))); + } + if (group.numlines >= 2) { + var fragment = document.createDocumentFragment(); + var br = document.createElement("br"); + var match = commit.author.match(/\b([A-Z])\B/g); + if (match) { + var text = document.createTextNode( + match.join('')); + } + if (br && text) { + var elem = fragment || td_sha1; + elem.appendChild(br); + elem.appendChild(text); + if (fragment) { + td_sha1.appendChild(fragment); + } + } + } + } else { + //tr.removeChild(td_sha1); // DOM2 Core way + tr.deleteCell(0); // DOM2 HTML way + } + + /* 123 */ + var linenr_commit = + ('previous' in commit ? commit.previous : commit.sha1); + var linenr_filename = + ('file_parent' in commit ? commit.file_parent : commit.filename); + a_linenr.href = projectUrl + 'a=blame_incremental' + + ';hb=' + linenr_commit + + ';f=' + encodeURIComponent(linenr_filename) + + '#l' + (group.srcline + i); + + blamedLines++; + + //updateProgressInfo(); + } +} + +// ---------------------------------------------------------------------- + +var inProgress = false; // are we processing response + +/**#@+ + * @constant + */ +var sha1Re = /^([0-9a-f]{40}) ([0-9]+) ([0-9]+) ([0-9]+)/; +var infoRe = /^([a-z-]+) ?(.*)/; +var endRe = /^END ?([^ ]*) ?(.*)/; +/**@-*/ + +var curCommit = new Commit(); +var curGroup = {}; + +var pollTimer = null; + +/** + * Parse output from 'git blame --incremental [...]', received via + * XMLHttpRequest from server (blamedataUrl), and call handleLine + * (which updates page) as soon as blame entry is completed. + * + * @param {String[]} lines: new complete lines from blamedata server + * + * @globals commits, curCommit, curGroup, t_interval_server, cmds_server + * @globals sha1Re, infoRe, endRe + */ +function processBlameLines(lines) { + var match; + + for (var i = 0, len = lines.length; i < len; i++) { + + if ((match = sha1Re.exec(lines[i]))) { + var sha1 = match[1]; + var srcline = parseInt(match[2], 10); + var resline = parseInt(match[3], 10); + var numlines = parseInt(match[4], 10); + + var c = commits[sha1]; + if (!c) { + c = new Commit(sha1); + commits[sha1] = c; + } + curCommit = c; + + curGroup.srcline = srcline; + curGroup.resline = resline; + curGroup.numlines = numlines; + + } else if ((match = infoRe.exec(lines[i]))) { + var info = match[1]; + var data = match[2]; + switch (info) { + case 'filename': + curCommit.filename = unquote(data); + // 'filename' information terminates the entry + handleLine(curCommit, curGroup); + updateProgressInfo(); + break; + case 'author': + curCommit.author = data; + break; + case 'author-time': + curCommit.authorTime = parseInt(data, 10); + break; + case 'author-tz': + curCommit.authorTimezone = data; + break; + case 'previous': + curCommit.nprevious++; + // store only first 'previous' header + if (!'previous' in curCommit) { + var parts = data.split(' ', 2); + curCommit.previous = parts[0]; + curCommit.file_parent = unquote(parts[1]); + } + break; + case 'boundary': + curCommit.boundary = true; + break; + } // end switch + + } else if ((match = endRe.exec(lines[i]))) { + t_interval_server = match[1]; + cmds_server = match[2]; + + } else if (lines[i] !== '') { + // malformed line + + } // end if (match) + + } // end for (lines) +} + +/** + * Process new data and return pointer to end of processed part + * + * @param {String} unprocessed: new data (from nextReadPos) + * @param {Number} nextReadPos: end of last processed data + * @return {Number} end of processed data (new value for nextReadPos) + */ +function processData(unprocessed, nextReadPos) { + var lastLineEnd = unprocessed.lastIndexOf('\n'); + if (lastLineEnd !== -1) { + var lines = unprocessed.substring(0, lastLineEnd).split('\n'); + nextReadPos += lastLineEnd + 1 /* 1 == '\n'.length */; + + processBlameLines(lines); + } // end if + + return nextReadPos; +} + +/** + * Handle XMLHttpRequest errors + * + * @param {XMLHttpRequest} xhr: XMLHttpRequest object + * + * @globals pollTimer, commits, inProgress + */ +function handleError(xhr) { + errorInfo('Server error: ' + + xhr.status + ' - ' + (xhr.statusText || 'Error contacting server')); + + clearInterval(pollTimer); + commits = {}; // free memory + + inProgress = false; +} + +/** + * Called after XMLHttpRequest finishes (loads) + * + * @param {XMLHttpRequest} xhr: XMLHttpRequest object (unused) + * + * @globals pollTimer, commits, inProgress + */ +function responseLoaded(xhr) { + clearInterval(pollTimer); + + fixColorsAndGroups(); + writeTimeInterval(); + commits = {}; // free memory + + inProgress = false; +} + +/** + * handler for XMLHttpRequest onreadystatechange event + * @see startBlame + * + * @globals xhr, inProgress + */ +function handleResponse() { + + /* + * xhr.readyState + * + * Value Constant (W3C) Description + * ------------------------------------------------------------------- + * 0 UNSENT open() has not been called yet. + * 1 OPENED send() has not been called yet. + * 2 HEADERS_RECEIVED send() has been called, and headers + * and status are available. + * 3 LOADING Downloading; responseText holds partial data. + * 4 DONE The operation is complete. + */ + + if (xhr.readyState !== 4 && xhr.readyState !== 3) { + return; + } + + // the server returned error + // try ... catch block is to work around bug in IE8 + try { + if (xhr.readyState === 3 && xhr.status !== 200) { + return; + } + } catch (e) { + return; + } + if (xhr.readyState === 4 && xhr.status !== 200) { + handleError(xhr); + return; + } + + // In konqueror xhr.responseText is sometimes null here... + if (xhr.responseText === null) { + return; + } + + // in case we were called before finished processing + if (inProgress) { + return; + } else { + inProgress = true; + } + + // extract new whole (complete) lines, and process them + while (xhr.prevDataLength !== xhr.responseText.length) { + if (xhr.readyState === 4 && + xhr.prevDataLength === xhr.responseText.length) { + break; + } + + xhr.prevDataLength = xhr.responseText.length; + var unprocessed = xhr.responseText.substring(xhr.nextReadPos); + xhr.nextReadPos = processData(unprocessed, xhr.nextReadPos); + } // end while + + // did we finish work? + if (xhr.readyState === 4 && + xhr.prevDataLength === xhr.responseText.length) { + responseLoaded(xhr); + } + + inProgress = false; +} + +// ============================================================ +// ------------------------------------------------------------ + +/** + * Incrementally update line data in blame_incremental view in gitweb. + * + * @param {String} blamedataUrl: URL to server script generating blame data. + * @param {String} bUrl: partial URL to project, used to generate links. + * + * Called from 'blame_incremental' view after loading table with + * file contents, a base for blame view. + * + * @globals xhr, t0, projectUrl, div_progress_bar, totalLines, pollTimer +*/ +function startBlame(blamedataUrl, bUrl) { + + xhr = createRequestObject(); + if (!xhr) { + errorInfo('ERROR: XMLHttpRequest not supported'); + return; + } + + t0 = new Date(); + projectUrl = bUrl + (bUrl.indexOf('?') === -1 ? '?' : ';'); + if ((div_progress_bar = document.getElementById('progress_bar'))) { + //div_progress_bar.setAttribute('style', 'width: 100%;'); + div_progress_bar.style.cssText = 'width: 100%;'; + } + totalLines = countLines(); + updateProgressInfo(); + + /* add extra properties to xhr object to help processing response */ + xhr.prevDataLength = -1; // used to detect if we have new data + xhr.nextReadPos = 0; // where unread part of response starts + + xhr.onreadystatechange = handleResponse; + //xhr.onreadystatechange = function () { handleResponse(xhr); }; + + xhr.open('GET', blamedataUrl); + xhr.setRequestHeader('Accept', 'text/plain'); + xhr.send(null); + + // not all browsers call onreadystatechange event on each server flush + // poll response using timer every second to handle this issue + pollTimer = setInterval(xhr.onreadystatechange, 1000); +} + +// end of gitweb.js diff --git a/t/gitweb-lib.sh b/t/gitweb-lib.sh index 5a734b1b7b..b70b891b62 100644 --- a/t/gitweb-lib.sh +++ b/t/gitweb-lib.sh @@ -19,9 +19,9 @@ our \$site_name = '[localhost]'; our \$site_header = ''; our \$site_footer = ''; our \$home_text = 'indextext.html'; -our @stylesheets = ('file:///$TEST_DIRECTORY/../gitweb/gitweb.css'); -our \$logo = 'file:///$TEST_DIRECTORY/../gitweb/git-logo.png'; -our \$favicon = 'file:///$TEST_DIRECTORY/../gitweb/git-favicon.png'; +our @stylesheets = ('file:///$TEST_DIRECTORY/../gitweb/static/gitweb.css'); +our \$logo = 'file:///$TEST_DIRECTORY/../gitweb/static/git-logo.png'; +our \$favicon = 'file:///$TEST_DIRECTORY/../gitweb/static/git-favicon.png'; our \$projects_list = ''; our \$export_ok = ''; our \$strict_export = ''; -- cgit v1.3 From 70649945c215b1674611a40eda1d0058118a6d1a Mon Sep 17 00:00:00 2001 From: Jay Soffian Date: Fri, 18 Jun 2010 17:01:25 -0400 Subject: gitweb/Makefile: fix typo in gitweb.min.css rule This typo has been in place since the rule was originally added by 0e6ce21 (Gitweb: add support for minifying gitweb.css). Signed-off-by: Jay Soffian Signed-off-by: Junio C Hamano --- gitweb/Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'gitweb/Makefile') diff --git a/gitweb/Makefile b/gitweb/Makefile index f2e1d92fbb..e7dd252773 100644 --- a/gitweb/Makefile +++ b/gitweb/Makefile @@ -91,7 +91,7 @@ ifdef CSSMIN GITWEB_CSS = gitweb.min.css all:: gitweb.min.css gitweb.min.css: gitweb.css GITWEB-BUILD-OPTIONS - $(QUIET_GEN)$(CSSMIN) <$ >$@ + $(QUIET_GEN)$(CSSMIN) <$< >$@ endif GITWEB_REPLACE = \ -- cgit v1.3 From 7ce896b3000a7bd2ea24f02ad3051f43ad351e1f Mon Sep 17 00:00:00 2001 From: Christopher Wilson Date: Tue, 21 Sep 2010 00:25:19 -0700 Subject: Enable highlight executable path as a configuration option Allow build-time/run-time configuration of the highlight executable (must be the one from http://www.andre-simon.de due to assumptions about parameters and output). Defaults to previous behavior which assumes that highlight is available on the server PATH. However, if this is not the case, the path to the highlight executable can be configured at build time as a configuration variable HIGHLIGHT_BIN = /path/to/highlight or at runtime by configuring GITWEB_CONFIG $highlight_bin = /path/to/highlight Signed-off-by: Christopher Wilson Acked-by: Jakub Narebski Signed-off-by: Junio C Hamano --- gitweb/Makefile | 4 +++- gitweb/README | 11 ++++++++++- gitweb/gitweb.perl | 9 ++++++++- 3 files changed, 21 insertions(+), 3 deletions(-) mode change 100644 => 100755 gitweb/README (limited to 'gitweb/Makefile') diff --git a/gitweb/Makefile b/gitweb/Makefile index 2fb7c2d77b..e32ee76309 100644 --- a/gitweb/Makefile +++ b/gitweb/Makefile @@ -35,6 +35,7 @@ GITWEB_FAVICON = static/git-favicon.png GITWEB_JS = static/gitweb.js GITWEB_SITE_HEADER = GITWEB_SITE_FOOTER = +HIGHLIGHT_BIN = highlight # include user config -include ../config.mak.autogen @@ -129,7 +130,8 @@ GITWEB_REPLACE = \ -e 's|++GITWEB_FAVICON++|$(GITWEB_FAVICON)|g' \ -e 's|++GITWEB_JS++|$(GITWEB_JS)|g' \ -e 's|++GITWEB_SITE_HEADER++|$(GITWEB_SITE_HEADER)|g' \ - -e 's|++GITWEB_SITE_FOOTER++|$(GITWEB_SITE_FOOTER)|g' + -e 's|++GITWEB_SITE_FOOTER++|$(GITWEB_SITE_FOOTER)|g' \ + -e 's|++HIGHLIGHT_BIN++|$(HIGHLIGHT_BIN)|g' GITWEB-BUILD-OPTIONS: FORCE @rm -f $@+ diff --git a/gitweb/README b/gitweb/README old mode 100644 new mode 100755 index d481198796..bf3664f2b7 --- a/gitweb/README +++ b/gitweb/README @@ -114,6 +114,11 @@ You can specify the following configuration variables when building GIT: when gitweb.cgi is executed, then the file specified in the environment variable will be loaded instead of the file specified when gitweb.cgi was created. [Default: /etc/gitweb.conf] + * HIGHLIGHT_BIN + Path to the highlight executable to use (must be the one from + http://www.andre-simon.de due to assumptions about parameters and output). + Useful if highlight is not installed on your webserver's PATH. + [Default: highlight] Runtime gitweb configuration @@ -236,7 +241,11 @@ not include variables usually directly set during build): If server load exceed this value then return "503 Service Unavailable" error. Server load is taken to be 0 if gitweb cannot determine its value. Set it to undefined value to turn it off. The default is 300. - + * $highlight_bin + Path to the highlight executable to use (must be the one from + http://www.andre-simon.de due to assumptions about parameters and output). + Useful if highlight is not installed on your webserver's PATH. + [Default: highlight] Projects list file format ~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/gitweb/gitweb.perl b/gitweb/gitweb.perl index a85e2f6319..e5910ce8f9 100755 --- a/gitweb/gitweb.perl +++ b/gitweb/gitweb.perl @@ -165,6 +165,12 @@ our @diff_opts = ('-M'); # taken from git_commit # the gitweb domain. our $prevent_xss = 0; +# Path to the highlight executable to use (must be the one from +# http://www.andre-simon.de due to assumptions about parameters and output). +# Useful if highlight is not installed on your webserver's PATH. +# [Default: highlight] +our $highlight_bin = "++HIGHLIGHT_BIN++"; + # information about snapshot formats that gitweb is capable of serving our %known_snapshot_formats = ( # name => { @@ -3360,7 +3366,8 @@ sub run_highlighter { close $fd or die_error(404, "Reading blob failed"); open $fd, quote_command(git_cmd(), "cat-file", "blob", $hash)." | ". - "highlight --xhtml --fragment --syntax $syntax |" + quote_command($highlight_bin). + " --xhtml --fragment --syntax $syntax |" or die_error(500, "Couldn't open file or run syntax highlighter"); return $fd; } -- cgit v1.3 From 958a8467216ca607ca6a24059d5c641347927333 Mon Sep 17 00:00:00 2001 From: Jakub Narebski Date: Sun, 26 Sep 2010 15:02:26 +0200 Subject: gitweb/Makefile: Add 'test' and 'test-installed' targets The 'test-installed' target in gitweb/Makefile tests installed gitweb, using the same destination directory that 'install' target uses. The 'test' target is just a convenience wrapper invoking 'gitweb-test' target of t/Makefile. Signed-off-by: Jakub Narebski Signed-off-by: Junio C Hamano --- gitweb/Makefile | 11 ++++++++++- t/Makefile | 4 ++++ 2 files changed, 14 insertions(+), 1 deletion(-) (limited to 'gitweb/Makefile') diff --git a/gitweb/Makefile b/gitweb/Makefile index 2fb7c2d77b..f2180741fc 100644 --- a/gitweb/Makefile +++ b/gitweb/Makefile @@ -143,6 +143,15 @@ gitweb.cgi: gitweb.perl GITWEB-BUILD-OPTIONS chmod +x $@+ && \ mv $@+ $@ +### Testing rules + +test: + $(MAKE) -C ../t gitweb-test + +test-installed: + GITWEB_TEST_INSTALLED='$(DESTDIR_SQ)$(gitwebdir_SQ)' \ + $(MAKE) -C ../t gitweb-test + ### Installation rules install: all @@ -156,5 +165,5 @@ install: all clean: $(RM) gitweb.cgi static/gitweb.min.js static/gitweb.min.css GITWEB-BUILD-OPTIONS -.PHONY: all clean install .FORCE-GIT-VERSION-FILE FORCE +.PHONY: all clean install test test-installed .FORCE-GIT-VERSION-FILE FORCE diff --git a/t/Makefile b/t/Makefile index c7baefb7ea..7aa409ab64 100644 --- a/t/Makefile +++ b/t/Makefile @@ -17,6 +17,7 @@ SHELL_PATH_SQ = $(subst ','\'',$(SHELL_PATH)) T = $(wildcard t[0-9][0-9][0-9][0-9]-*.sh) TSVN = $(wildcard t91[0-9][0-9]-*.sh) +TGITWEB = $(wildcard t95[0-9][0-9]-*.sh) all: pre-clean $(MAKE) aggregate-results-and-cleanup @@ -46,6 +47,9 @@ full-svn-test: $(MAKE) $(TSVN) GIT_SVN_NO_OPTIMIZE_COMMITS=1 LC_ALL=C $(MAKE) $(TSVN) GIT_SVN_NO_OPTIMIZE_COMMITS=0 LC_ALL=en_US.UTF-8 +gitweb-test: + $(MAKE) $(TGITWEB) + valgrind: GIT_TEST_OPTS=--valgrind $(MAKE) -- cgit v1.3 From 9b93aeb29cea16880926bc150ac31cfb31475a44 Mon Sep 17 00:00:00 2001 From: Jakub Narebski Date: Sun, 26 Sep 2010 15:02:24 +0200 Subject: gitweb/Makefile: Include gitweb/config.mak Allow for gitweb-specific Makefile config to reside in config.mak file in the 'gitweb/' subdirectory. This means that gitweb-specific build-time configuration variable can reside in gitweb-specific gitweb/config.mak Signed-off-by: Jakub Narebski Signed-off-by: Junio C Hamano --- gitweb/Makefile | 1 + 1 file changed, 1 insertion(+) (limited to 'gitweb/Makefile') diff --git a/gitweb/Makefile b/gitweb/Makefile index f2180741fc..df908a18eb 100644 --- a/gitweb/Makefile +++ b/gitweb/Makefile @@ -39,6 +39,7 @@ GITWEB_SITE_FOOTER = # include user config -include ../config.mak.autogen -include ../config.mak +-include config.mak # determine version ../GIT-VERSION-FILE: .FORCE-GIT-VERSION-FILE -- cgit v1.3 From 9a86dd571058a511e60af35206a31017d873e54a Mon Sep 17 00:00:00 2001 From: Jakub Narebski Date: Thu, 28 Apr 2011 21:04:01 +0200 Subject: gitweb: Split JavaScript for maintability, combining on build Split originally single gitweb.js file into smaller files, each dealing with single issue / area of responsibility. This move should make gitweb's JavaScript code easier to maintain. For better webapp performance it is recommended[1][2][3] to combine JavaScript files. Do it during build time (in gitweb/Makefile), by straight concatenation of files into gitweb.js file (which is now ignored as being generated). This means that there are no changes to gitweb script itself - it still uses gitweb.js or gitweb.min.js, but now generated. [1]: http://developer.yahoo.com/performance/rules.html "Minimize HTTP Requests" section [2]: http://code.google.com/speed/articles/include-scripts-properly.html "1. Combine external JavaScript files" [3]: http://javascript-reference.info/speed-up-your-javascript-load-time.htm "Combine Your Files" section. See also new gitweb/static/js/README file. Inspired-by-patch-by: John 'Warthog9' Hawley Signed-off-by: Jakub Narebski Signed-off-by: Junio C Hamano --- .gitignore | 1 + gitweb/Makefile | 16 +- gitweb/static/gitweb.js | 889 ------------------------------- gitweb/static/js/README | 20 + gitweb/static/js/blame_incremental.js | 687 ++++++++++++++++++++++++ gitweb/static/js/javascript-detection.js | 43 ++ gitweb/static/js/lib/common-lib.js | 187 +++++++ 7 files changed, 953 insertions(+), 890 deletions(-) delete mode 100644 gitweb/static/gitweb.js create mode 100644 gitweb/static/js/README create mode 100644 gitweb/static/js/blame_incremental.js create mode 100644 gitweb/static/js/javascript-detection.js create mode 100644 gitweb/static/js/lib/common-lib.js (limited to 'gitweb/Makefile') diff --git a/.gitignore b/.gitignore index 2cf3ca5808..9e9500540e 100644 --- a/.gitignore +++ b/.gitignore @@ -158,6 +158,7 @@ /gitk-git/gitk-wish /gitweb/GITWEB-BUILD-OPTIONS /gitweb/gitweb.cgi +/gitweb/static/gitweb.js /gitweb/static/gitweb.min.* /test-chmtime /test-ctype diff --git a/gitweb/Makefile b/gitweb/Makefile index 0a6ac00631..0baa9df9f6 100644 --- a/gitweb/Makefile +++ b/gitweb/Makefile @@ -86,7 +86,7 @@ ifndef V endif endif -all:: gitweb.cgi +all:: gitweb.cgi static/gitweb.js GITWEB_PROGRAMS = gitweb.cgi @@ -112,6 +112,15 @@ endif GITWEB_FILES += static/git-logo.png static/git-favicon.png +# JavaScript files that are composed (concatenated) to form gitweb.js +# +# js/lib/common-lib.js should be always first, then js/lib/*.js, +# then the rest of files; js/gitweb.js should be last (if it exists) +GITWEB_JSLIB_FILES += static/js/lib/common-lib.js +GITWEB_JSLIB_FILES += static/js/javascript-detection.js +GITWEB_JSLIB_FILES += static/js/blame_incremental.js + + GITWEB_REPLACE = \ -e 's|++GIT_VERSION++|$(GIT_VERSION)|g' \ -e 's|++GIT_BINDIR++|$(bindir)|g' \ @@ -146,6 +155,11 @@ gitweb.cgi: gitweb.perl GITWEB-BUILD-OPTIONS chmod +x $@+ && \ mv $@+ $@ +static/gitweb.js: $(GITWEB_JSLIB_FILES) + $(QUIET_GEN)$(RM) $@ $@+ && \ + cat $^ >$@+ && \ + mv $@+ $@ + ### Testing rules test: diff --git a/gitweb/static/gitweb.js b/gitweb/static/gitweb.js deleted file mode 100644 index 40ec08440b..0000000000 --- a/gitweb/static/gitweb.js +++ /dev/null @@ -1,889 +0,0 @@ -// Copyright (C) 2007, Fredrik Kuivinen -// 2007, Petr Baudis -// 2008-2009, Jakub Narebski - -/** - * @fileOverview JavaScript code for gitweb (git web interface). - * @license GPLv2 or later - */ - -/* ============================================================ */ -/* functions for generic gitweb actions and views */ - -/** - * used to check if link has 'js' query parameter already (at end), - * and other reasons to not add 'js=1' param at the end of link - * @constant - */ -var jsExceptionsRe = /[;?]js=[01]$/; - -/** - * Add '?js=1' or ';js=1' to the end of every link in the document - * that doesn't have 'js' query parameter set already. - * - * Links with 'js=1' lead to JavaScript version of given action, if it - * exists (currently there is only 'blame_incremental' for 'blame') - * - * @globals jsExceptionsRe - */ -function fixLinks() { - var allLinks = document.getElementsByTagName("a") || document.links; - for (var i = 0, len = allLinks.length; i < len; i++) { - var link = allLinks[i]; - if (!jsExceptionsRe.test(link)) { // =~ /[;?]js=[01]$/; - link.href += - (link.href.indexOf('?') === -1 ? '?' : ';') + 'js=1'; - } - } -} - - -/* ============================================================ */ - -/* - * This code uses DOM methods instead of (nonstandard) innerHTML - * to modify page. - * - * innerHTML is non-standard IE extension, though supported by most - * browsers; however Firefox up to version 1.5 didn't implement it in - * a strict mode (application/xml+xhtml mimetype). - * - * Also my simple benchmarks show that using elem.firstChild.data = - * 'content' is slightly faster than elem.innerHTML = 'content'. It - * is however more fragile (text element fragment must exists), and - * less feature-rich (we cannot add HTML). - * - * Note that DOM 2 HTML is preferred over generic DOM 2 Core; the - * equivalent using DOM 2 Core is usually shown in comments. - */ - - -/* ============================================================ */ -/* generic utility functions */ - - -/** - * pad number N with nonbreakable spaces on the left, to WIDTH characters - * example: padLeftStr(12, 3, '\u00A0') == '\u00A012' - * ('\u00A0' is nonbreakable space) - * - * @param {Number|String} input: number to pad - * @param {Number} width: visible width of output - * @param {String} str: string to prefix to string, e.g. '\u00A0' - * @returns {String} INPUT prefixed with (WIDTH - INPUT.length) x STR - */ -function padLeftStr(input, width, str) { - var prefix = ''; - - width -= input.toString().length; - while (width > 0) { - prefix += str; - width--; - } - return prefix + input; -} - -/** - * Pad INPUT on the left to SIZE width, using given padding character CH, - * for example padLeft('a', 3, '_') is '__a'. - * - * @param {String} input: input value converted to string. - * @param {Number} width: desired length of output. - * @param {String} ch: single character to prefix to string. - * - * @returns {String} Modified string, at least SIZE length. - */ -function padLeft(input, width, ch) { - var s = input + ""; - while (s.length < width) { - s = ch + s; - } - return s; -} - -/** - * Create XMLHttpRequest object in cross-browser way - * @returns XMLHttpRequest object, or null - */ -function createRequestObject() { - try { - return new XMLHttpRequest(); - } catch (e) {} - try { - return window.createRequest(); - } catch (e) {} - try { - return new ActiveXObject("Msxml2.XMLHTTP"); - } catch (e) {} - try { - return new ActiveXObject("Microsoft.XMLHTTP"); - } catch (e) {} - - return null; -} - - -/* ============================================================ */ -/* utility/helper functions (and variables) */ - -var xhr; // XMLHttpRequest object -var projectUrl; // partial query + separator ('?' or ';') - -// 'commits' is an associative map. It maps SHA1s to Commit objects. -var commits = {}; - -/** - * constructor for Commit objects, used in 'blame' - * @class Represents a blamed commit - * @param {String} sha1: SHA-1 identifier of a commit - */ -function Commit(sha1) { - if (this instanceof Commit) { - this.sha1 = sha1; - this.nprevious = 0; /* number of 'previous', effective parents */ - } else { - return new Commit(sha1); - } -} - -/* ............................................................ */ -/* progress info, timing, error reporting */ - -var blamedLines = 0; -var totalLines = '???'; -var div_progress_bar; -var div_progress_info; - -/** - * Detects how many lines does a blamed file have, - * This information is used in progress info - * - * @returns {Number|String} Number of lines in file, or string '...' - */ -function countLines() { - var table = - document.getElementById('blame_table') || - document.getElementsByTagName('table')[0]; - - if (table) { - return table.getElementsByTagName('tr').length - 1; // for header - } else { - return '...'; - } -} - -/** - * update progress info and length (width) of progress bar - * - * @globals div_progress_info, div_progress_bar, blamedLines, totalLines - */ -function updateProgressInfo() { - if (!div_progress_info) { - div_progress_info = document.getElementById('progress_info'); - } - if (!div_progress_bar) { - div_progress_bar = document.getElementById('progress_bar'); - } - if (!div_progress_info && !div_progress_bar) { - return; - } - - var percentage = Math.floor(100.0*blamedLines/totalLines); - - if (div_progress_info) { - div_progress_info.firstChild.data = blamedLines + ' / ' + totalLines + - ' (' + padLeftStr(percentage, 3, '\u00A0') + '%)'; - } - - if (div_progress_bar) { - //div_progress_bar.setAttribute('style', 'width: '+percentage+'%;'); - div_progress_bar.style.width = percentage + '%'; - } -} - - -var t_interval_server = ''; -var cmds_server = ''; -var t0 = new Date(); - -/** - * write how much it took to generate data, and to run script - * - * @globals t0, t_interval_server, cmds_server - */ -function writeTimeInterval() { - var info_time = document.getElementById('generating_time'); - if (!info_time || !t_interval_server) { - return; - } - var t1 = new Date(); - info_time.firstChild.data += ' + (' + - t_interval_server + ' sec server blame_data / ' + - (t1.getTime() - t0.getTime())/1000 + ' sec client JavaScript)'; - - var info_cmds = document.getElementById('generating_cmd'); - if (!info_time || !cmds_server) { - return; - } - info_cmds.firstChild.data += ' + ' + cmds_server; -} - -/** - * show an error message alert to user within page (in prohress info area) - * @param {String} str: plain text error message (no HTML) - * - * @globals div_progress_info - */ -function errorInfo(str) { - if (!div_progress_info) { - div_progress_info = document.getElementById('progress_info'); - } - if (div_progress_info) { - div_progress_info.className = 'error'; - div_progress_info.firstChild.data = str; - } -} - -/* ............................................................ */ -/* coloring rows during blame_data (git blame --incremental) run */ - -/** - * used to extract N from 'colorN', where N is a number, - * @constant - */ -var colorRe = /\bcolor([0-9]*)\b/; - -/** - * return N if , otherwise return null - * (some browsers require CSS class names to begin with letter) - * - * @param {HTMLElement} tr: table row element to check - * @param {String} tr.className: 'class' attribute of tr element - * @returns {Number|null} N if tr.className == 'colorN', otherwise null - * - * @globals colorRe - */ -function getColorNo(tr) { - if (!tr) { - return null; - } - var className = tr.className; - if (className) { - var match = colorRe.exec(className); - if (match) { - return parseInt(match[1], 10); - } - } - return null; -} - -var colorsFreq = [0, 0, 0]; -/** - * return one of given possible colors (curently least used one) - * example: chooseColorNoFrom(2, 3) returns 2 or 3 - * - * @param {Number[]} arguments: one or more numbers - * assumes that 1 <= arguments[i] <= colorsFreq.length - * @returns {Number} Least used color number from arguments - * @globals colorsFreq - */ -function chooseColorNoFrom() { - // choose the color which is least used - var colorNo = arguments[0]; - for (var i = 1; i < arguments.length; i++) { - if (colorsFreq[arguments[i]-1] < colorsFreq[colorNo-1]) { - colorNo = arguments[i]; - } - } - colorsFreq[colorNo-1]++; - return colorNo; -} - -/** - * given two neigbour elements, find color which would be different - * from color of both of neighbours; used to 3-color blame table - * - * @param {HTMLElement} tr_prev - * @param {HTMLElement} tr_next - * @returns {Number} color number N such that - * colorN != tr_prev.className && colorN != tr_next.className - */ -function findColorNo(tr_prev, tr_next) { - var color_prev = getColorNo(tr_prev); - var color_next = getColorNo(tr_next); - - - // neither of neighbours has color set - // THEN we can use any of 3 possible colors - if (!color_prev && !color_next) { - return chooseColorNoFrom(1,2,3); - } - - // either both neighbours have the same color, - // or only one of neighbours have color set - // THEN we can use any color except given - var color; - if (color_prev === color_next) { - color = color_prev; // = color_next; - } else if (!color_prev) { - color = color_next; - } else if (!color_next) { - color = color_prev; - } - if (color) { - return chooseColorNoFrom((color % 3) + 1, ((color+1) % 3) + 1); - } - - // neighbours have different colors - // THEN there is only one color left - return (3 - ((color_prev + color_next) % 3)); -} - -/* ............................................................ */ -/* coloring rows like 'blame' after 'blame_data' finishes */ - -/** - * returns true if given row element (tr) is first in commit group - * to be used only after 'blame_data' finishes (after processing) - * - * @param {HTMLElement} tr: table row - * @returns {Boolean} true if TR is first in commit group - */ -function isStartOfGroup(tr) { - return tr.firstChild.className === 'sha1'; -} - -/** - * change colors to use zebra coloring (2 colors) instead of 3 colors - * concatenate neighbour commit groups belonging to the same commit - * - * @globals colorRe - */ -function fixColorsAndGroups() { - var colorClasses = ['light', 'dark']; - var linenum = 1; - var tr, prev_group; - var colorClass = 0; - var table = - document.getElementById('blame_table') || - document.getElementsByTagName('table')[0]; - - while ((tr = document.getElementById('l'+linenum))) { - // index origin is 0, which is table header; start from 1 - //while ((tr = table.rows[linenum])) { // <- it is slower - if (isStartOfGroup(tr, linenum, document)) { - if (prev_group && - prev_group.firstChild.firstChild.href === - tr.firstChild.firstChild.href) { - // we have to concatenate groups - var prev_rows = prev_group.firstChild.rowSpan || 1; - var curr_rows = tr.firstChild.rowSpan || 1; - prev_group.firstChild.rowSpan = prev_rows + curr_rows; - //tr.removeChild(tr.firstChild); - tr.deleteCell(0); // DOM2 HTML way - } else { - colorClass = (colorClass + 1) % 2; - prev_group = tr; - } - } - var tr_class = tr.className; - tr.className = tr_class.replace(colorRe, colorClasses[colorClass]); - linenum++; - } -} - -/* ............................................................ */ -/* time and data */ - -/** - * used to extract hours and minutes from timezone info, e.g '-0900' - * @constant - */ -var tzRe = /^([+-])([0-9][0-9])([0-9][0-9])$/; - -/** - * convert numeric timezone +/-ZZZZ to offset from UTC in seconds - * - * @param {String} timezoneInfo: numeric timezone '(+|-)HHMM' - * @returns {Number} offset from UTC in seconds for timezone - * - * @globals tzRe - */ -function timezoneOffset(timezoneInfo) { - var match = tzRe.exec(timezoneInfo); - var tz_sign = (match[1] === '-' ? -1 : +1); - var tz_hour = parseInt(match[2],10); - var tz_min = parseInt(match[3],10); - - return tz_sign*(((tz_hour*60) + tz_min)*60); -} - -/** - * return date in local time formatted in iso-8601 like format - * 'yyyy-mm-dd HH:MM:SS +/-ZZZZ' e.g. '2005-08-07 21:49:46 +0200' - * - * @param {Number} epoch: seconds since '00:00:00 1970-01-01 UTC' - * @param {String} timezoneInfo: numeric timezone '(+|-)HHMM' - * @returns {String} date in local time in iso-8601 like format - */ -function formatDateISOLocal(epoch, timezoneInfo) { - // date corrected by timezone - var localDate = new Date(1000 * (epoch + - timezoneOffset(timezoneInfo))); - var localDateStr = // e.g. '2005-08-07' - localDate.getUTCFullYear() + '-' + - padLeft(localDate.getUTCMonth()+1, 2, '0') + '-' + - padLeft(localDate.getUTCDate(), 2, '0'); - var localTimeStr = // e.g. '21:49:46' - padLeft(localDate.getUTCHours(), 2, '0') + ':' + - padLeft(localDate.getUTCMinutes(), 2, '0') + ':' + - padLeft(localDate.getUTCSeconds(), 2, '0'); - - return localDateStr + ' ' + localTimeStr + ' ' + timezoneInfo; -} - -/* ............................................................ */ -/* unquoting/unescaping filenames */ - -/**#@+ - * @constant - */ -var escCodeRe = /\\([^0-7]|[0-7]{1,3})/g; -var octEscRe = /^[0-7]{1,3}$/; -var maybeQuotedRe = /^\"(.*)\"$/; -/**#@-*/ - -/** - * unquote maybe git-quoted filename - * e.g. 'aa' -> 'aa', '"a\ta"' -> 'a a' - * - * @param {String} str: git-quoted string - * @returns {String} Unquoted and unescaped string - * - * @globals escCodeRe, octEscRe, maybeQuotedRe - */ -function unquote(str) { - function unq(seq) { - var es = { - // character escape codes, aka escape sequences (from C) - // replacements are to some extent JavaScript specific - t: "\t", // tab (HT, TAB) - n: "\n", // newline (NL) - r: "\r", // return (CR) - f: "\f", // form feed (FF) - b: "\b", // backspace (BS) - a: "\x07", // alarm (bell) (BEL) - e: "\x1B", // escape (ESC) - v: "\v" // vertical tab (VT) - }; - - if (seq.search(octEscRe) !== -1) { - // octal char sequence - return String.fromCharCode(parseInt(seq, 8)); - } else if (seq in es) { - // C escape sequence, aka character escape code - return es[seq]; - } - // quoted ordinary character - return seq; - } - - var match = str.match(maybeQuotedRe); - if (match) { - str = match[1]; - // perhaps str = eval('"'+str+'"'); would be enough? - str = str.replace(escCodeRe, - function (substr, p1, offset, s) { return unq(p1); }); - } - return str; -} - -/* ============================================================ */ -/* main part: parsing response */ - -/** - * Function called for each blame entry, as soon as it finishes. - * It updates page via DOM manipulation, adding sha1 info, etc. - * - * @param {Commit} commit: blamed commit - * @param {Object} group: object representing group of lines, - * which blame the same commit (blame entry) - * - * @globals blamedLines - */ -function handleLine(commit, group) { - /* - This is the structure of the HTML fragment we are working - with: - - - - 123 - # times (my ext3 doesn't). - - */ - - var resline = group.resline; - - // format date and time string only once per commit - if (!commit.info) { - /* e.g. 'Kay Sievers, 2005-08-07 21:49:46 +0200' */ - commit.info = commit.author + ', ' + - formatDateISOLocal(commit.authorTime, commit.authorTimezone); - } - - // color depends on group of lines, not only on blamed commit - var colorNo = findColorNo( - document.getElementById('l'+(resline-1)), - document.getElementById('l'+(resline+group.numlines)) - ); - - // loop over lines in commit group - for (var i = 0; i < group.numlines; i++, resline++) { - var tr = document.getElementById('l'+resline); - if (!tr) { - break; - } - /* - - - 123 - # times (my ext3 doesn't). - - */ - var td_sha1 = tr.firstChild; - var a_sha1 = td_sha1.firstChild; - var a_linenr = td_sha1.nextSibling.firstChild; - - /* */ - var tr_class = ''; - if (colorNo !== null) { - tr_class = 'color'+colorNo; - } - if (commit.boundary) { - tr_class += ' boundary'; - } - if (commit.nprevious === 0) { - tr_class += ' no-previous'; - } else if (commit.nprevious > 1) { - tr_class += ' multiple-previous'; - } - tr.className = tr_class; - - /* ? */ - if (i === 0) { - td_sha1.title = commit.info; - td_sha1.rowSpan = group.numlines; - - a_sha1.href = projectUrl + 'a=commit;h=' + commit.sha1; - if (a_sha1.firstChild) { - a_sha1.firstChild.data = commit.sha1.substr(0, 8); - } else { - a_sha1.appendChild( - document.createTextNode(commit.sha1.substr(0, 8))); - } - if (group.numlines >= 2) { - var fragment = document.createDocumentFragment(); - var br = document.createElement("br"); - var match = commit.author.match(/\b([A-Z])\B/g); - if (match) { - var text = document.createTextNode( - match.join('')); - } - if (br && text) { - var elem = fragment || td_sha1; - elem.appendChild(br); - elem.appendChild(text); - if (fragment) { - td_sha1.appendChild(fragment); - } - } - } - } else { - //tr.removeChild(td_sha1); // DOM2 Core way - tr.deleteCell(0); // DOM2 HTML way - } - - /* 123 */ - var linenr_commit = - ('previous' in commit ? commit.previous : commit.sha1); - var linenr_filename = - ('file_parent' in commit ? commit.file_parent : commit.filename); - a_linenr.href = projectUrl + 'a=blame_incremental' + - ';hb=' + linenr_commit + - ';f=' + encodeURIComponent(linenr_filename) + - '#l' + (group.srcline + i); - - blamedLines++; - - //updateProgressInfo(); - } -} - -// ---------------------------------------------------------------------- - -var inProgress = false; // are we processing response - -/**#@+ - * @constant - */ -var sha1Re = /^([0-9a-f]{40}) ([0-9]+) ([0-9]+) ([0-9]+)/; -var infoRe = /^([a-z-]+) ?(.*)/; -var endRe = /^END ?([^ ]*) ?(.*)/; -/**@-*/ - -var curCommit = new Commit(); -var curGroup = {}; - -var pollTimer = null; - -/** - * Parse output from 'git blame --incremental [...]', received via - * XMLHttpRequest from server (blamedataUrl), and call handleLine - * (which updates page) as soon as blame entry is completed. - * - * @param {String[]} lines: new complete lines from blamedata server - * - * @globals commits, curCommit, curGroup, t_interval_server, cmds_server - * @globals sha1Re, infoRe, endRe - */ -function processBlameLines(lines) { - var match; - - for (var i = 0, len = lines.length; i < len; i++) { - - if ((match = sha1Re.exec(lines[i]))) { - var sha1 = match[1]; - var srcline = parseInt(match[2], 10); - var resline = parseInt(match[3], 10); - var numlines = parseInt(match[4], 10); - - var c = commits[sha1]; - if (!c) { - c = new Commit(sha1); - commits[sha1] = c; - } - curCommit = c; - - curGroup.srcline = srcline; - curGroup.resline = resline; - curGroup.numlines = numlines; - - } else if ((match = infoRe.exec(lines[i]))) { - var info = match[1]; - var data = match[2]; - switch (info) { - case 'filename': - curCommit.filename = unquote(data); - // 'filename' information terminates the entry - handleLine(curCommit, curGroup); - updateProgressInfo(); - break; - case 'author': - curCommit.author = data; - break; - case 'author-time': - curCommit.authorTime = parseInt(data, 10); - break; - case 'author-tz': - curCommit.authorTimezone = data; - break; - case 'previous': - curCommit.nprevious++; - // store only first 'previous' header - if (!'previous' in curCommit) { - var parts = data.split(' ', 2); - curCommit.previous = parts[0]; - curCommit.file_parent = unquote(parts[1]); - } - break; - case 'boundary': - curCommit.boundary = true; - break; - } // end switch - - } else if ((match = endRe.exec(lines[i]))) { - t_interval_server = match[1]; - cmds_server = match[2]; - - } else if (lines[i] !== '') { - // malformed line - - } // end if (match) - - } // end for (lines) -} - -/** - * Process new data and return pointer to end of processed part - * - * @param {String} unprocessed: new data (from nextReadPos) - * @param {Number} nextReadPos: end of last processed data - * @return {Number} end of processed data (new value for nextReadPos) - */ -function processData(unprocessed, nextReadPos) { - var lastLineEnd = unprocessed.lastIndexOf('\n'); - if (lastLineEnd !== -1) { - var lines = unprocessed.substring(0, lastLineEnd).split('\n'); - nextReadPos += lastLineEnd + 1 /* 1 == '\n'.length */; - - processBlameLines(lines); - } // end if - - return nextReadPos; -} - -/** - * Handle XMLHttpRequest errors - * - * @param {XMLHttpRequest} xhr: XMLHttpRequest object - * - * @globals pollTimer, commits, inProgress - */ -function handleError(xhr) { - errorInfo('Server error: ' + - xhr.status + ' - ' + (xhr.statusText || 'Error contacting server')); - - clearInterval(pollTimer); - commits = {}; // free memory - - inProgress = false; -} - -/** - * Called after XMLHttpRequest finishes (loads) - * - * @param {XMLHttpRequest} xhr: XMLHttpRequest object (unused) - * - * @globals pollTimer, commits, inProgress - */ -function responseLoaded(xhr) { - clearInterval(pollTimer); - - fixColorsAndGroups(); - writeTimeInterval(); - commits = {}; // free memory - - inProgress = false; -} - -/** - * handler for XMLHttpRequest onreadystatechange event - * @see startBlame - * - * @globals xhr, inProgress - */ -function handleResponse() { - - /* - * xhr.readyState - * - * Value Constant (W3C) Description - * ------------------------------------------------------------------- - * 0 UNSENT open() has not been called yet. - * 1 OPENED send() has not been called yet. - * 2 HEADERS_RECEIVED send() has been called, and headers - * and status are available. - * 3 LOADING Downloading; responseText holds partial data. - * 4 DONE The operation is complete. - */ - - if (xhr.readyState !== 4 && xhr.readyState !== 3) { - return; - } - - // the server returned error - // try ... catch block is to work around bug in IE8 - try { - if (xhr.readyState === 3 && xhr.status !== 200) { - return; - } - } catch (e) { - return; - } - if (xhr.readyState === 4 && xhr.status !== 200) { - handleError(xhr); - return; - } - - // In konqueror xhr.responseText is sometimes null here... - if (xhr.responseText === null) { - return; - } - - // in case we were called before finished processing - if (inProgress) { - return; - } else { - inProgress = true; - } - - // extract new whole (complete) lines, and process them - while (xhr.prevDataLength !== xhr.responseText.length) { - if (xhr.readyState === 4 && - xhr.prevDataLength === xhr.responseText.length) { - break; - } - - xhr.prevDataLength = xhr.responseText.length; - var unprocessed = xhr.responseText.substring(xhr.nextReadPos); - xhr.nextReadPos = processData(unprocessed, xhr.nextReadPos); - } // end while - - // did we finish work? - if (xhr.readyState === 4 && - xhr.prevDataLength === xhr.responseText.length) { - responseLoaded(xhr); - } - - inProgress = false; -} - -// ============================================================ -// ------------------------------------------------------------ - -/** - * Incrementally update line data in blame_incremental view in gitweb. - * - * @param {String} blamedataUrl: URL to server script generating blame data. - * @param {String} bUrl: partial URL to project, used to generate links. - * - * Called from 'blame_incremental' view after loading table with - * file contents, a base for blame view. - * - * @globals xhr, t0, projectUrl, div_progress_bar, totalLines, pollTimer -*/ -function startBlame(blamedataUrl, bUrl) { - - xhr = createRequestObject(); - if (!xhr) { - errorInfo('ERROR: XMLHttpRequest not supported'); - return; - } - - t0 = new Date(); - projectUrl = bUrl + (bUrl.indexOf('?') === -1 ? '?' : ';'); - if ((div_progress_bar = document.getElementById('progress_bar'))) { - //div_progress_bar.setAttribute('style', 'width: 100%;'); - div_progress_bar.style.cssText = 'width: 100%;'; - } - totalLines = countLines(); - updateProgressInfo(); - - /* add extra properties to xhr object to help processing response */ - xhr.prevDataLength = -1; // used to detect if we have new data - xhr.nextReadPos = 0; // where unread part of response starts - - xhr.onreadystatechange = handleResponse; - //xhr.onreadystatechange = function () { handleResponse(xhr); }; - - xhr.open('GET', blamedataUrl); - xhr.setRequestHeader('Accept', 'text/plain'); - xhr.send(null); - - // not all browsers call onreadystatechange event on each server flush - // poll response using timer every second to handle this issue - pollTimer = setInterval(xhr.onreadystatechange, 1000); -} - -// end of gitweb.js diff --git a/gitweb/static/js/README b/gitweb/static/js/README new file mode 100644 index 0000000000..f8460ed32f --- /dev/null +++ b/gitweb/static/js/README @@ -0,0 +1,20 @@ +GIT web interface (gitweb) - JavaScript +======================================= + +This directory holds JavaScript code used by gitweb (GIT web interface). +Scripts from there would be concatenated together in the order specified +by gitweb/Makefile into gitweb/static/gitweb.js, during building of +gitweb/gitweb.cgi (during gitweb building). The resulting file (or its +minification) would then be installed / deployed together with gitweb. + +Scripts in 'lib/' subdirectory compose generic JavaScript library, +providing features required by gitweb but in no way limited to gitweb +only. In the future those scripts could be replaced by some JavaScript +library / framework, like e.g. jQuery, YUI, Prototype, MooTools, Dojo, +ExtJS, Script.aculo.us or SproutCore. + +All scripts that manipulate gitweb output should be put outside 'lib/', +directly in this directory ('gitweb/static/js/'). Those scripts would +have to be rewritten if gitweb moves to using some JavaScript library. + +See also comments in gitweb/Makefile. diff --git a/gitweb/static/js/blame_incremental.js b/gitweb/static/js/blame_incremental.js new file mode 100644 index 0000000000..f63f78b9ec --- /dev/null +++ b/gitweb/static/js/blame_incremental.js @@ -0,0 +1,687 @@ +// Copyright (C) 2007, Fredrik Kuivinen +// 2007, Petr Baudis +// 2008-2011, Jakub Narebski + +/** + * @fileOverview JavaScript side of Ajax-y 'blame_incremental' view in gitweb + * @license GPLv2 or later + */ + + +/* + * This code uses DOM methods instead of (nonstandard) innerHTML + * to modify page. + * + * innerHTML is non-standard IE extension, though supported by most + * browsers; however Firefox up to version 1.5 didn't implement it in + * a strict mode (application/xml+xhtml mimetype). + * + * Also my simple benchmarks show that using elem.firstChild.data = + * 'content' is slightly faster than elem.innerHTML = 'content'. It + * is however more fragile (text element fragment must exists), and + * less feature-rich (we cannot add HTML). + * + * Note that DOM 2 HTML is preferred over generic DOM 2 Core; the + * equivalent using DOM 2 Core is usually shown in comments. + */ + + +/* ============================================================ */ +/* utility/helper functions (and variables) */ + +var xhr; // XMLHttpRequest object +var projectUrl; // partial query + separator ('?' or ';') + +// 'commits' is an associative map. It maps SHA1s to Commit objects. +var commits = {}; + +/** + * constructor for Commit objects, used in 'blame' + * @class Represents a blamed commit + * @param {String} sha1: SHA-1 identifier of a commit + */ +function Commit(sha1) { + if (this instanceof Commit) { + this.sha1 = sha1; + this.nprevious = 0; /* number of 'previous', effective parents */ + } else { + return new Commit(sha1); + } +} + +/* ............................................................ */ +/* progress info, timing, error reporting */ + +var blamedLines = 0; +var totalLines = '???'; +var div_progress_bar; +var div_progress_info; + +/** + * Detects how many lines does a blamed file have, + * This information is used in progress info + * + * @returns {Number|String} Number of lines in file, or string '...' + */ +function countLines() { + var table = + document.getElementById('blame_table') || + document.getElementsByTagName('table')[0]; + + if (table) { + return table.getElementsByTagName('tr').length - 1; // for header + } else { + return '...'; + } +} + +/** + * update progress info and length (width) of progress bar + * + * @globals div_progress_info, div_progress_bar, blamedLines, totalLines + */ +function updateProgressInfo() { + if (!div_progress_info) { + div_progress_info = document.getElementById('progress_info'); + } + if (!div_progress_bar) { + div_progress_bar = document.getElementById('progress_bar'); + } + if (!div_progress_info && !div_progress_bar) { + return; + } + + var percentage = Math.floor(100.0*blamedLines/totalLines); + + if (div_progress_info) { + div_progress_info.firstChild.data = blamedLines + ' / ' + totalLines + + ' (' + padLeftStr(percentage, 3, '\u00A0') + '%)'; + } + + if (div_progress_bar) { + //div_progress_bar.setAttribute('style', 'width: '+percentage+'%;'); + div_progress_bar.style.width = percentage + '%'; + } +} + + +var t_interval_server = ''; +var cmds_server = ''; +var t0 = new Date(); + +/** + * write how much it took to generate data, and to run script + * + * @globals t0, t_interval_server, cmds_server + */ +function writeTimeInterval() { + var info_time = document.getElementById('generating_time'); + if (!info_time || !t_interval_server) { + return; + } + var t1 = new Date(); + info_time.firstChild.data += ' + (' + + t_interval_server + ' sec server blame_data / ' + + (t1.getTime() - t0.getTime())/1000 + ' sec client JavaScript)'; + + var info_cmds = document.getElementById('generating_cmd'); + if (!info_time || !cmds_server) { + return; + } + info_cmds.firstChild.data += ' + ' + cmds_server; +} + +/** + * show an error message alert to user within page (in prohress info area) + * @param {String} str: plain text error message (no HTML) + * + * @globals div_progress_info + */ +function errorInfo(str) { + if (!div_progress_info) { + div_progress_info = document.getElementById('progress_info'); + } + if (div_progress_info) { + div_progress_info.className = 'error'; + div_progress_info.firstChild.data = str; + } +} + +/* ............................................................ */ +/* coloring rows during blame_data (git blame --incremental) run */ + +/** + * used to extract N from 'colorN', where N is a number, + * @constant + */ +var colorRe = /\bcolor([0-9]*)\b/; + +/** + * return N if , otherwise return null + * (some browsers require CSS class names to begin with letter) + * + * @param {HTMLElement} tr: table row element to check + * @param {String} tr.className: 'class' attribute of tr element + * @returns {Number|null} N if tr.className == 'colorN', otherwise null + * + * @globals colorRe + */ +function getColorNo(tr) { + if (!tr) { + return null; + } + var className = tr.className; + if (className) { + var match = colorRe.exec(className); + if (match) { + return parseInt(match[1], 10); + } + } + return null; +} + +var colorsFreq = [0, 0, 0]; +/** + * return one of given possible colors (curently least used one) + * example: chooseColorNoFrom(2, 3) returns 2 or 3 + * + * @param {Number[]} arguments: one or more numbers + * assumes that 1 <= arguments[i] <= colorsFreq.length + * @returns {Number} Least used color number from arguments + * @globals colorsFreq + */ +function chooseColorNoFrom() { + // choose the color which is least used + var colorNo = arguments[0]; + for (var i = 1; i < arguments.length; i++) { + if (colorsFreq[arguments[i]-1] < colorsFreq[colorNo-1]) { + colorNo = arguments[i]; + } + } + colorsFreq[colorNo-1]++; + return colorNo; +} + +/** + * given two neigbour elements, find color which would be different + * from color of both of neighbours; used to 3-color blame table + * + * @param {HTMLElement} tr_prev + * @param {HTMLElement} tr_next + * @returns {Number} color number N such that + * colorN != tr_prev.className && colorN != tr_next.className + */ +function findColorNo(tr_prev, tr_next) { + var color_prev = getColorNo(tr_prev); + var color_next = getColorNo(tr_next); + + + // neither of neighbours has color set + // THEN we can use any of 3 possible colors + if (!color_prev && !color_next) { + return chooseColorNoFrom(1,2,3); + } + + // either both neighbours have the same color, + // or only one of neighbours have color set + // THEN we can use any color except given + var color; + if (color_prev === color_next) { + color = color_prev; // = color_next; + } else if (!color_prev) { + color = color_next; + } else if (!color_next) { + color = color_prev; + } + if (color) { + return chooseColorNoFrom((color % 3) + 1, ((color+1) % 3) + 1); + } + + // neighbours have different colors + // THEN there is only one color left + return (3 - ((color_prev + color_next) % 3)); +} + +/* ............................................................ */ +/* coloring rows like 'blame' after 'blame_data' finishes */ + +/** + * returns true if given row element (tr) is first in commit group + * to be used only after 'blame_data' finishes (after processing) + * + * @param {HTMLElement} tr: table row + * @returns {Boolean} true if TR is first in commit group + */ +function isStartOfGroup(tr) { + return tr.firstChild.className === 'sha1'; +} + +/** + * change colors to use zebra coloring (2 colors) instead of 3 colors + * concatenate neighbour commit groups belonging to the same commit + * + * @globals colorRe + */ +function fixColorsAndGroups() { + var colorClasses = ['light', 'dark']; + var linenum = 1; + var tr, prev_group; + var colorClass = 0; + var table = + document.getElementById('blame_table') || + document.getElementsByTagName('table')[0]; + + while ((tr = document.getElementById('l'+linenum))) { + // index origin is 0, which is table header; start from 1 + //while ((tr = table.rows[linenum])) { // <- it is slower + if (isStartOfGroup(tr, linenum, document)) { + if (prev_group && + prev_group.firstChild.firstChild.href === + tr.firstChild.firstChild.href) { + // we have to concatenate groups + var prev_rows = prev_group.firstChild.rowSpan || 1; + var curr_rows = tr.firstChild.rowSpan || 1; + prev_group.firstChild.rowSpan = prev_rows + curr_rows; + //tr.removeChild(tr.firstChild); + tr.deleteCell(0); // DOM2 HTML way + } else { + colorClass = (colorClass + 1) % 2; + prev_group = tr; + } + } + var tr_class = tr.className; + tr.className = tr_class.replace(colorRe, colorClasses[colorClass]); + linenum++; + } +} + + +/* ============================================================ */ +/* main part: parsing response */ + +/** + * Function called for each blame entry, as soon as it finishes. + * It updates page via DOM manipulation, adding sha1 info, etc. + * + * @param {Commit} commit: blamed commit + * @param {Object} group: object representing group of lines, + * which blame the same commit (blame entry) + * + * @globals blamedLines + */ +function handleLine(commit, group) { + /* + This is the structure of the HTML fragment we are working + with: + + + + 123 + # times (my ext3 doesn't). + + */ + + var resline = group.resline; + + // format date and time string only once per commit + if (!commit.info) { + /* e.g. 'Kay Sievers, 2005-08-07 21:49:46 +0200' */ + commit.info = commit.author + ', ' + + formatDateISOLocal(commit.authorTime, commit.authorTimezone); + } + + // color depends on group of lines, not only on blamed commit + var colorNo = findColorNo( + document.getElementById('l'+(resline-1)), + document.getElementById('l'+(resline+group.numlines)) + ); + + // loop over lines in commit group + for (var i = 0; i < group.numlines; i++, resline++) { + var tr = document.getElementById('l'+resline); + if (!tr) { + break; + } + /* + + + 123 + # times (my ext3 doesn't). + + */ + var td_sha1 = tr.firstChild; + var a_sha1 = td_sha1.firstChild; + var a_linenr = td_sha1.nextSibling.firstChild; + + /* */ + var tr_class = ''; + if (colorNo !== null) { + tr_class = 'color'+colorNo; + } + if (commit.boundary) { + tr_class += ' boundary'; + } + if (commit.nprevious === 0) { + tr_class += ' no-previous'; + } else if (commit.nprevious > 1) { + tr_class += ' multiple-previous'; + } + tr.className = tr_class; + + /* ? */ + if (i === 0) { + td_sha1.title = commit.info; + td_sha1.rowSpan = group.numlines; + + a_sha1.href = projectUrl + 'a=commit;h=' + commit.sha1; + if (a_sha1.firstChild) { + a_sha1.firstChild.data = commit.sha1.substr(0, 8); + } else { + a_sha1.appendChild( + document.createTextNode(commit.sha1.substr(0, 8))); + } + if (group.numlines >= 2) { + var fragment = document.createDocumentFragment(); + var br = document.createElement("br"); + var match = commit.author.match(/\b([A-Z])\B/g); + if (match) { + var text = document.createTextNode( + match.join('')); + } + if (br && text) { + var elem = fragment || td_sha1; + elem.appendChild(br); + elem.appendChild(text); + if (fragment) { + td_sha1.appendChild(fragment); + } + } + } + } else { + //tr.removeChild(td_sha1); // DOM2 Core way + tr.deleteCell(0); // DOM2 HTML way + } + + /* 123 */ + var linenr_commit = + ('previous' in commit ? commit.previous : commit.sha1); + var linenr_filename = + ('file_parent' in commit ? commit.file_parent : commit.filename); + a_linenr.href = projectUrl + 'a=blame_incremental' + + ';hb=' + linenr_commit + + ';f=' + encodeURIComponent(linenr_filename) + + '#l' + (group.srcline + i); + + blamedLines++; + + //updateProgressInfo(); + } +} + +// ---------------------------------------------------------------------- + +var inProgress = false; // are we processing response + +/**#@+ + * @constant + */ +var sha1Re = /^([0-9a-f]{40}) ([0-9]+) ([0-9]+) ([0-9]+)/; +var infoRe = /^([a-z-]+) ?(.*)/; +var endRe = /^END ?([^ ]*) ?(.*)/; +/**@-*/ + +var curCommit = new Commit(); +var curGroup = {}; + +var pollTimer = null; + +/** + * Parse output from 'git blame --incremental [...]', received via + * XMLHttpRequest from server (blamedataUrl), and call handleLine + * (which updates page) as soon as blame entry is completed. + * + * @param {String[]} lines: new complete lines from blamedata server + * + * @globals commits, curCommit, curGroup, t_interval_server, cmds_server + * @globals sha1Re, infoRe, endRe + */ +function processBlameLines(lines) { + var match; + + for (var i = 0, len = lines.length; i < len; i++) { + + if ((match = sha1Re.exec(lines[i]))) { + var sha1 = match[1]; + var srcline = parseInt(match[2], 10); + var resline = parseInt(match[3], 10); + var numlines = parseInt(match[4], 10); + + var c = commits[sha1]; + if (!c) { + c = new Commit(sha1); + commits[sha1] = c; + } + curCommit = c; + + curGroup.srcline = srcline; + curGroup.resline = resline; + curGroup.numlines = numlines; + + } else if ((match = infoRe.exec(lines[i]))) { + var info = match[1]; + var data = match[2]; + switch (info) { + case 'filename': + curCommit.filename = unquote(data); + // 'filename' information terminates the entry + handleLine(curCommit, curGroup); + updateProgressInfo(); + break; + case 'author': + curCommit.author = data; + break; + case 'author-time': + curCommit.authorTime = parseInt(data, 10); + break; + case 'author-tz': + curCommit.authorTimezone = data; + break; + case 'previous': + curCommit.nprevious++; + // store only first 'previous' header + if (!'previous' in curCommit) { + var parts = data.split(' ', 2); + curCommit.previous = parts[0]; + curCommit.file_parent = unquote(parts[1]); + } + break; + case 'boundary': + curCommit.boundary = true; + break; + } // end switch + + } else if ((match = endRe.exec(lines[i]))) { + t_interval_server = match[1]; + cmds_server = match[2]; + + } else if (lines[i] !== '') { + // malformed line + + } // end if (match) + + } // end for (lines) +} + +/** + * Process new data and return pointer to end of processed part + * + * @param {String} unprocessed: new data (from nextReadPos) + * @param {Number} nextReadPos: end of last processed data + * @return {Number} end of processed data (new value for nextReadPos) + */ +function processData(unprocessed, nextReadPos) { + var lastLineEnd = unprocessed.lastIndexOf('\n'); + if (lastLineEnd !== -1) { + var lines = unprocessed.substring(0, lastLineEnd).split('\n'); + nextReadPos += lastLineEnd + 1 /* 1 == '\n'.length */; + + processBlameLines(lines); + } // end if + + return nextReadPos; +} + +/** + * Handle XMLHttpRequest errors + * + * @param {XMLHttpRequest} xhr: XMLHttpRequest object + * + * @globals pollTimer, commits, inProgress + */ +function handleError(xhr) { + errorInfo('Server error: ' + + xhr.status + ' - ' + (xhr.statusText || 'Error contacting server')); + + clearInterval(pollTimer); + commits = {}; // free memory + + inProgress = false; +} + +/** + * Called after XMLHttpRequest finishes (loads) + * + * @param {XMLHttpRequest} xhr: XMLHttpRequest object (unused) + * + * @globals pollTimer, commits, inProgress + */ +function responseLoaded(xhr) { + clearInterval(pollTimer); + + fixColorsAndGroups(); + writeTimeInterval(); + commits = {}; // free memory + + inProgress = false; +} + +/** + * handler for XMLHttpRequest onreadystatechange event + * @see startBlame + * + * @globals xhr, inProgress + */ +function handleResponse() { + + /* + * xhr.readyState + * + * Value Constant (W3C) Description + * ------------------------------------------------------------------- + * 0 UNSENT open() has not been called yet. + * 1 OPENED send() has not been called yet. + * 2 HEADERS_RECEIVED send() has been called, and headers + * and status are available. + * 3 LOADING Downloading; responseText holds partial data. + * 4 DONE The operation is complete. + */ + + if (xhr.readyState !== 4 && xhr.readyState !== 3) { + return; + } + + // the server returned error + // try ... catch block is to work around bug in IE8 + try { + if (xhr.readyState === 3 && xhr.status !== 200) { + return; + } + } catch (e) { + return; + } + if (xhr.readyState === 4 && xhr.status !== 200) { + handleError(xhr); + return; + } + + // In konqueror xhr.responseText is sometimes null here... + if (xhr.responseText === null) { + return; + } + + // in case we were called before finished processing + if (inProgress) { + return; + } else { + inProgress = true; + } + + // extract new whole (complete) lines, and process them + while (xhr.prevDataLength !== xhr.responseText.length) { + if (xhr.readyState === 4 && + xhr.prevDataLength === xhr.responseText.length) { + break; + } + + xhr.prevDataLength = xhr.responseText.length; + var unprocessed = xhr.responseText.substring(xhr.nextReadPos); + xhr.nextReadPos = processData(unprocessed, xhr.nextReadPos); + } // end while + + // did we finish work? + if (xhr.readyState === 4 && + xhr.prevDataLength === xhr.responseText.length) { + responseLoaded(xhr); + } + + inProgress = false; +} + +// ============================================================ +// ------------------------------------------------------------ + +/** + * Incrementally update line data in blame_incremental view in gitweb. + * + * @param {String} blamedataUrl: URL to server script generating blame data. + * @param {String} bUrl: partial URL to project, used to generate links. + * + * Called from 'blame_incremental' view after loading table with + * file contents, a base for blame view. + * + * @globals xhr, t0, projectUrl, div_progress_bar, totalLines, pollTimer +*/ +function startBlame(blamedataUrl, bUrl) { + + xhr = createRequestObject(); + if (!xhr) { + errorInfo('ERROR: XMLHttpRequest not supported'); + return; + } + + t0 = new Date(); + projectUrl = bUrl + (bUrl.indexOf('?') === -1 ? '?' : ';'); + if ((div_progress_bar = document.getElementById('progress_bar'))) { + //div_progress_bar.setAttribute('style', 'width: 100%;'); + div_progress_bar.style.cssText = 'width: 100%;'; + } + totalLines = countLines(); + updateProgressInfo(); + + /* add extra properties to xhr object to help processing response */ + xhr.prevDataLength = -1; // used to detect if we have new data + xhr.nextReadPos = 0; // where unread part of response starts + + xhr.onreadystatechange = handleResponse; + //xhr.onreadystatechange = function () { handleResponse(xhr); }; + + xhr.open('GET', blamedataUrl); + xhr.setRequestHeader('Accept', 'text/plain'); + xhr.send(null); + + // not all browsers call onreadystatechange event on each server flush + // poll response using timer every second to handle this issue + pollTimer = setInterval(xhr.onreadystatechange, 1000); +} + +/* end of blame_incremental.js */ diff --git a/gitweb/static/js/javascript-detection.js b/gitweb/static/js/javascript-detection.js new file mode 100644 index 0000000000..93dd2bdd91 --- /dev/null +++ b/gitweb/static/js/javascript-detection.js @@ -0,0 +1,43 @@ +// Copyright (C) 2007, Fredrik Kuivinen +// 2007, Petr Baudis +// 2008-2011, Jakub Narebski + +/** + * @fileOverview Detect if JavaScript is enabled, and pass it to server-side + * @license GPLv2 or later + */ + + +/* ============================================================ */ +/* Manipulating links */ + +/** + * used to check if link has 'js' query parameter already (at end), + * and other reasons to not add 'js=1' param at the end of link + * @constant + */ +var jsExceptionsRe = /[;?]js=[01]$/; + +/** + * Add '?js=1' or ';js=1' to the end of every link in the document + * that doesn't have 'js' query parameter set already. + * + * Links with 'js=1' lead to JavaScript version of given action, if it + * exists (currently there is only 'blame_incremental' for 'blame') + * + * To be used as `window.onload` handler + * + * @globals jsExceptionsRe + */ +function fixLinks() { + var allLinks = document.getElementsByTagName("a") || document.links; + for (var i = 0, len = allLinks.length; i < len; i++) { + var link = allLinks[i]; + if (!jsExceptionsRe.test(link)) { // =~ /[;?]js=[01]$/; + link.href += + (link.href.indexOf('?') === -1 ? '?' : ';') + 'js=1'; + } + } +} + +/* end of javascript-detection.js */ diff --git a/gitweb/static/js/lib/common-lib.js b/gitweb/static/js/lib/common-lib.js new file mode 100644 index 0000000000..38f3b9e144 --- /dev/null +++ b/gitweb/static/js/lib/common-lib.js @@ -0,0 +1,187 @@ +// Copyright (C) 2007, Fredrik Kuivinen +// 2007, Petr Baudis +// 2008-2011, Jakub Narebski + +/** + * @fileOverview Generic JavaScript code (helper functions) + * @license GPLv2 or later + */ + + +/* ============================================================ */ +/* ............................................................ */ +/* Padding */ + +/** + * pad number N with nonbreakable spaces on the left, to WIDTH characters + * example: padLeftStr(12, 3, '\u00A0') == '\u00A012' + * ('\u00A0' is nonbreakable space) + * + * @param {Number|String} input: number to pad + * @param {Number} width: visible width of output + * @param {String} str: string to prefix to string, e.g. '\u00A0' + * @returns {String} INPUT prefixed with (WIDTH - INPUT.length) x STR + */ +function padLeftStr(input, width, str) { + var prefix = ''; + + width -= input.toString().length; + while (width > 0) { + prefix += str; + width--; + } + return prefix + input; +} + +/** + * Pad INPUT on the left to SIZE width, using given padding character CH, + * for example padLeft('a', 3, '_') is '__a'. + * + * @param {String} input: input value converted to string. + * @param {Number} width: desired length of output. + * @param {String} ch: single character to prefix to string. + * + * @returns {String} Modified string, at least SIZE length. + */ +function padLeft(input, width, ch) { + var s = input + ""; + while (s.length < width) { + s = ch + s; + } + return s; +} + + +/* ............................................................ */ +/* Ajax */ + +/** + * Create XMLHttpRequest object in cross-browser way + * @returns XMLHttpRequest object, or null + */ +function createRequestObject() { + try { + return new XMLHttpRequest(); + } catch (e) {} + try { + return window.createRequest(); + } catch (e) {} + try { + return new ActiveXObject("Msxml2.XMLHTTP"); + } catch (e) {} + try { + return new ActiveXObject("Microsoft.XMLHTTP"); + } catch (e) {} + + return null; +} + + +/* ............................................................ */ +/* time and data */ + +/** + * used to extract hours and minutes from timezone info, e.g '-0900' + * @constant + */ +var tzRe = /^([+-])([0-9][0-9])([0-9][0-9])$/; + +/** + * convert numeric timezone +/-ZZZZ to offset from UTC in seconds + * + * @param {String} timezoneInfo: numeric timezone '(+|-)HHMM' + * @returns {Number} offset from UTC in seconds for timezone + * + * @globals tzRe + */ +function timezoneOffset(timezoneInfo) { + var match = tzRe.exec(timezoneInfo); + var tz_sign = (match[1] === '-' ? -1 : +1); + var tz_hour = parseInt(match[2],10); + var tz_min = parseInt(match[3],10); + + return tz_sign*(((tz_hour*60) + tz_min)*60); +} + +/** + * return date in local time formatted in iso-8601 like format + * 'yyyy-mm-dd HH:MM:SS +/-ZZZZ' e.g. '2005-08-07 21:49:46 +0200' + * + * @param {Number} epoch: seconds since '00:00:00 1970-01-01 UTC' + * @param {String} timezoneInfo: numeric timezone '(+|-)HHMM' + * @returns {String} date in local time in iso-8601 like format + */ +function formatDateISOLocal(epoch, timezoneInfo) { + // date corrected by timezone + var localDate = new Date(1000 * (epoch + + timezoneOffset(timezoneInfo))); + var localDateStr = // e.g. '2005-08-07' + localDate.getUTCFullYear() + '-' + + padLeft(localDate.getUTCMonth()+1, 2, '0') + '-' + + padLeft(localDate.getUTCDate(), 2, '0'); + var localTimeStr = // e.g. '21:49:46' + padLeft(localDate.getUTCHours(), 2, '0') + ':' + + padLeft(localDate.getUTCMinutes(), 2, '0') + ':' + + padLeft(localDate.getUTCSeconds(), 2, '0'); + + return localDateStr + ' ' + localTimeStr + ' ' + timezoneInfo; +} + + +/* ............................................................ */ +/* unquoting/unescaping filenames */ + +/**#@+ + * @constant + */ +var escCodeRe = /\\([^0-7]|[0-7]{1,3})/g; +var octEscRe = /^[0-7]{1,3}$/; +var maybeQuotedRe = /^\"(.*)\"$/; +/**#@-*/ + +/** + * unquote maybe git-quoted filename + * e.g. 'aa' -> 'aa', '"a\ta"' -> 'a a' + * + * @param {String} str: git-quoted string + * @returns {String} Unquoted and unescaped string + * + * @globals escCodeRe, octEscRe, maybeQuotedRe + */ +function unquote(str) { + function unq(seq) { + var es = { + // character escape codes, aka escape sequences (from C) + // replacements are to some extent JavaScript specific + t: "\t", // tab (HT, TAB) + n: "\n", // newline (NL) + r: "\r", // return (CR) + f: "\f", // form feed (FF) + b: "\b", // backspace (BS) + a: "\x07", // alarm (bell) (BEL) + e: "\x1B", // escape (ESC) + v: "\v" // vertical tab (VT) + }; + + if (seq.search(octEscRe) !== -1) { + // octal char sequence + return String.fromCharCode(parseInt(seq, 8)); + } else if (seq in es) { + // C escape sequence, aka character escape code + return es[seq]; + } + // quoted ordinary character + return seq; + } + + var match = str.match(maybeQuotedRe); + if (match) { + str = match[1]; + // perhaps str = eval('"'+str+'"'); would be enough? + str = str.replace(escCodeRe, + function (substr, p1, offset, s) { return unq(p1); }); + } + return str; +} + +/* end of common-lib.js */ -- cgit v1.3 From 54b1479a770b240d71bbd000fb40e470987bac59 Mon Sep 17 00:00:00 2001 From: Jakub Narebski Date: Thu, 28 Apr 2011 21:04:04 +0200 Subject: gitweb.js: Extract and improve datetime handling Move formatDateISOLocal(epoch, timezone) function (and also helper timezoneOffset(timezoneInfo) function it requires) from common-lib.js to datetime.js Add new functions: * localTimezoneOffset - to get browser timezone offset in seconds * localTimezoneInfo - to get browser timezone in '(+|-)HHMM' format * formatTimezoneInfo - turn offset in hours and minutes into '(+|-)HHMM' * parseRFC2822Date - to parse RFC-2822 dates that gitweb uses into epoch * formatDateRFC2882 - like formatDateISOLocal, only RFC-2822 format All those functions are meant to be used in future commit 'gitweb: javascript ability to adjust time based on timezone' An alternative would be to use e.g. Datejs (http://www.datejs.com) library, or JavaScript framework that has date formatting (perhaps as a plugin). While at it escape '-' in character class inside tzRe regexp, as recommended by JSLint (http://www.jslint.com). Signed-off-by: Jakub Narebski Signed-off-by: Junio C Hamano --- gitweb/Makefile | 1 + gitweb/static/js/lib/common-lib.js | 51 ------------ gitweb/static/js/lib/datetime.js | 161 +++++++++++++++++++++++++++++++++++++ 3 files changed, 162 insertions(+), 51 deletions(-) create mode 100644 gitweb/static/js/lib/datetime.js (limited to 'gitweb/Makefile') diff --git a/gitweb/Makefile b/gitweb/Makefile index 0baa9df9f6..403265a9b2 100644 --- a/gitweb/Makefile +++ b/gitweb/Makefile @@ -117,6 +117,7 @@ GITWEB_FILES += static/git-logo.png static/git-favicon.png # js/lib/common-lib.js should be always first, then js/lib/*.js, # then the rest of files; js/gitweb.js should be last (if it exists) GITWEB_JSLIB_FILES += static/js/lib/common-lib.js +GITWEB_JSLIB_FILES += static/js/lib/datetime.js GITWEB_JSLIB_FILES += static/js/javascript-detection.js GITWEB_JSLIB_FILES += static/js/blame_incremental.js diff --git a/gitweb/static/js/lib/common-lib.js b/gitweb/static/js/lib/common-lib.js index c45454ead2..d6b0c0d46a 100644 --- a/gitweb/static/js/lib/common-lib.js +++ b/gitweb/static/js/lib/common-lib.js @@ -88,57 +88,6 @@ function createRequestObject() { } -/* ............................................................ */ -/* time and data */ - -/** - * used to extract hours and minutes from timezone info, e.g '-0900' - * @constant - */ -var tzRe = /^([+-])([0-9][0-9])([0-9][0-9])$/; - -/** - * convert numeric timezone +/-ZZZZ to offset from UTC in seconds - * - * @param {String} timezoneInfo: numeric timezone '(+|-)HHMM' - * @returns {Number} offset from UTC in seconds for timezone - * - * @globals tzRe - */ -function timezoneOffset(timezoneInfo) { - var match = tzRe.exec(timezoneInfo); - var tz_sign = (match[1] === '-' ? -1 : +1); - var tz_hour = parseInt(match[2],10); - var tz_min = parseInt(match[3],10); - - return tz_sign*(((tz_hour*60) + tz_min)*60); -} - -/** - * return date in local time formatted in iso-8601 like format - * 'yyyy-mm-dd HH:MM:SS +/-ZZZZ' e.g. '2005-08-07 21:49:46 +0200' - * - * @param {Number} epoch: seconds since '00:00:00 1970-01-01 UTC' - * @param {String} timezoneInfo: numeric timezone '(+|-)HHMM' - * @returns {String} date in local time in iso-8601 like format - */ -function formatDateISOLocal(epoch, timezoneInfo) { - // date corrected by timezone - var localDate = new Date(1000 * (epoch + - timezoneOffset(timezoneInfo))); - var localDateStr = // e.g. '2005-08-07' - localDate.getUTCFullYear() + '-' + - padLeft(localDate.getUTCMonth()+1, 2, '0') + '-' + - padLeft(localDate.getUTCDate(), 2, '0'); - var localTimeStr = // e.g. '21:49:46' - padLeft(localDate.getUTCHours(), 2, '0') + ':' + - padLeft(localDate.getUTCMinutes(), 2, '0') + ':' + - padLeft(localDate.getUTCSeconds(), 2, '0'); - - return localDateStr + ' ' + localTimeStr + ' ' + timezoneInfo; -} - - /* ............................................................ */ /* unquoting/unescaping filenames */ diff --git a/gitweb/static/js/lib/datetime.js b/gitweb/static/js/lib/datetime.js new file mode 100644 index 0000000000..b3dcedb141 --- /dev/null +++ b/gitweb/static/js/lib/datetime.js @@ -0,0 +1,161 @@ +// Copyright (C) 2007, Fredrik Kuivinen +// 2007, Petr Baudis +// 2008-2011, Jakub Narebski + +/** + * @fileOverview Datetime manipulation: parsing and formatting + * @license GPLv2 or later + */ + + +/* ............................................................ */ +/* parsing and retrieving datetime related information */ + +/** + * used to extract hours and minutes from timezone info, e.g '-0900' + * @constant + */ +var tzRe = /^([+\-])([0-9][0-9])([0-9][0-9])$/; + +/** + * convert numeric timezone +/-ZZZZ to offset from UTC in seconds + * + * @param {String} timezoneInfo: numeric timezone '(+|-)HHMM' + * @returns {Number} offset from UTC in seconds for timezone + * + * @globals tzRe + */ +function timezoneOffset(timezoneInfo) { + var match = tzRe.exec(timezoneInfo); + var tz_sign = (match[1] === '-' ? -1 : +1); + var tz_hour = parseInt(match[2],10); + var tz_min = parseInt(match[3],10); + + return tz_sign*(((tz_hour*60) + tz_min)*60); +} + +/** + * return local (browser) timezone as offset from UTC in seconds + * + * @returns {Number} offset from UTC in seconds for local timezone + */ +function localTimezoneOffset() { + // getTimezoneOffset returns the time-zone offset from UTC, + // in _minutes_, for the current locale + return ((new Date()).getTimezoneOffset() * -60); +} + +/** + * return local (browser) timezone as numeric timezone '(+|-)HHMM' + * + * @returns {String} locat timezone as -/+ZZZZ + */ +function localTimezoneInfo() { + var tzOffsetMinutes = (new Date()).getTimezoneOffset() * -1; + + return formatTimezoneInfo(0, tzOffsetMinutes); +} + + +/** + * Parse RFC-2822 date into a Unix timestamp (into epoch) + * + * @param {String} date: date in RFC-2822 format, e.g. 'Thu, 21 Dec 2000 16:01:07 +0200' + * @returns {Number} epoch i.e. seconds since '00:00:00 1970-01-01 UTC' + */ +function parseRFC2822Date(date) { + // Date.parse accepts the IETF standard (RFC 1123 Section 5.2.14 and elsewhere) + // date syntax, which is defined in RFC 2822 (obsoletes RFC 822) + // and returns number of _milli_seconds since January 1, 1970, 00:00:00 UTC + return Date.parse(date) / 1000; +} + + +/* ............................................................ */ +/* formatting date */ + +/** + * format timezone offset as numerical timezone '(+|-)HHMM' or '(+|-)HH:MM' + * + * @param {Number} hours: offset in hours, e.g. 2 for '+0200' + * @param {Number} [minutes] offset in minutes, e.g. 30 for '-4030'; + * it is split into hours if not 0 <= minutes < 60, + * for example 1200 would give '+0100'; + * defaults to 0 + * @param {String} [sep] separator between hours and minutes part, + * default is '', might be ':' for W3CDTF (rfc-3339) + * @returns {String} timezone in '(+|-)HHMM' or '(+|-)HH:MM' format + */ +function formatTimezoneInfo(hours, minutes, sep) { + minutes = minutes || 0; // to be able to use formatTimezoneInfo(hh) + sep = sep || ''; // default format is +/-ZZZZ + + if (minutes < 0 || minutes > 59) { + hours = minutes > 0 ? Math.floor(minutes / 60) : Math.ceil(minutes / 60); + minutes = Math.abs(minutes - 60*hours); // sign of minutes is sign of hours + // NOTE: this works correctly because there is no UTC-00:30 timezone + } + + var tzSign = hours >= 0 ? '+' : '-'; + if (hours < 0) { + hours = -hours; // sign is stored in tzSign + } + + return tzSign + padLeft(hours, 2, '0') + sep + padLeft(minutes, 2, '0'); +} + +/** + * return date in local time formatted in iso-8601 like format + * 'yyyy-mm-dd HH:MM:SS +/-ZZZZ' e.g. '2005-08-07 21:49:46 +0200' + * + * @param {Number} epoch: seconds since '00:00:00 1970-01-01 UTC' + * @param {String} timezoneInfo: numeric timezone '(+|-)HHMM' + * @returns {String} date in local time in iso-8601 like format + */ +function formatDateISOLocal(epoch, timezoneInfo) { + // date corrected by timezone + var localDate = new Date(1000 * (epoch + + timezoneOffset(timezoneInfo))); + var localDateStr = // e.g. '2005-08-07' + localDate.getUTCFullYear() + '-' + + padLeft(localDate.getUTCMonth()+1, 2, '0') + '-' + + padLeft(localDate.getUTCDate(), 2, '0'); + var localTimeStr = // e.g. '21:49:46' + padLeft(localDate.getUTCHours(), 2, '0') + ':' + + padLeft(localDate.getUTCMinutes(), 2, '0') + ':' + + padLeft(localDate.getUTCSeconds(), 2, '0'); + + return localDateStr + ' ' + localTimeStr + ' ' + timezoneInfo; +} + +/** + * return date in local time formatted in rfc-2822 format + * e.g. 'Thu, 21 Dec 2000 16:01:07 +0200' + * + * @param {Number} epoch: seconds since '00:00:00 1970-01-01 UTC' + * @param {String} timezoneInfo: numeric timezone '(+|-)HHMM' + * @param {Boolean} [padDay] e.g. 'Sun, 07 Aug' if true, 'Sun, 7 Aug' otherwise + * @returns {String} date in local time in rfc-2822 format + */ +function formatDateRFC2882(epoch, timezoneInfo, padDay) { + // A short textual representation of a month, three letters + var months = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]; + // A textual representation of a day, three letters + var days = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"]; + // date corrected by timezone + var localDate = new Date(1000 * (epoch + + timezoneOffset(timezoneInfo))); + var localDateStr = // e.g. 'Sun, 7 Aug 2005' or 'Sun, 07 Aug 2005' + days[localDate.getUTCDay()] + ', ' + + (padDay ? padLeft(localDate.getUTCDate(),2,'0') : localDate.getUTCDate()) + ' ' + + months[localDate.getUTCMonth()] + ' ' + + localDate.getUTCFullYear(); + var localTimeStr = // e.g. '21:49:46' + padLeft(localDate.getUTCHours(), 2, '0') + ':' + + padLeft(localDate.getUTCMinutes(), 2, '0') + ':' + + padLeft(localDate.getUTCSeconds(), 2, '0'); + + return localDateStr + ' ' + localTimeStr + ' ' + timezoneInfo; +} + +/* end of datetime.js */ -- cgit v1.3 From fcce886bfb4671c4c17a3f99cb7caf0f7ab94e16 Mon Sep 17 00:00:00 2001 From: Jakub Narebski Date: Thu, 28 Apr 2011 21:04:05 +0200 Subject: gitweb.js: Introduce code to handle cookies from JavaScript Introduced gitweb/static/js/cookies.js file provides functions for setting, getting and deleting cookies. Code taken from subsection "Cookies in JavaScript" of "Professional JavaScript for Web Developers" by Nicholas C. Zakas and from cookie plugin for jQuery (dual licensed under the MIT and GPL licenses). Signed-off-by: Jakub Narebski Signed-off-by: Junio C Hamano --- gitweb/Makefile | 1 + gitweb/static/js/lib/cookies.js | 114 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 115 insertions(+) create mode 100644 gitweb/static/js/lib/cookies.js (limited to 'gitweb/Makefile') diff --git a/gitweb/Makefile b/gitweb/Makefile index 403265a9b2..7dd1dee55a 100644 --- a/gitweb/Makefile +++ b/gitweb/Makefile @@ -118,6 +118,7 @@ GITWEB_FILES += static/git-logo.png static/git-favicon.png # then the rest of files; js/gitweb.js should be last (if it exists) GITWEB_JSLIB_FILES += static/js/lib/common-lib.js GITWEB_JSLIB_FILES += static/js/lib/datetime.js +GITWEB_JSLIB_FILES += static/js/lib/cookies.js GITWEB_JSLIB_FILES += static/js/javascript-detection.js GITWEB_JSLIB_FILES += static/js/blame_incremental.js diff --git a/gitweb/static/js/lib/cookies.js b/gitweb/static/js/lib/cookies.js new file mode 100644 index 0000000000..72b51cd1b4 --- /dev/null +++ b/gitweb/static/js/lib/cookies.js @@ -0,0 +1,114 @@ +/** + * @fileOverview Accessing cookies from JavaScript + * @license GPLv2 or later + */ + +/* + * Based on subsection "Cookies in JavaScript" of "Professional + * JavaScript for Web Developers" by Nicholas C. Zakas and cookie + * plugin from jQuery (dual licensed under the MIT and GPL licenses) + */ + + +/** + * Create a cookie with the given name and value, + * and other optional parameters. + * + * @example + * setCookie('foo', 'bar'); // will be deleted when browser exits + * setCookie('foo', 'bar', { expires: new Date(Date.parse('Jan 1, 2012')) }); + * setCookie('foo', 'bar', { expires: 7 }); // 7 days = 1 week + * setCookie('foo', 'bar', { expires: 14, path: '/' }); + * + * @param {String} sName: Unique name of a cookie (letters, numbers, underscores). + * @param {String} sValue: The string value stored in a cookie. + * @param {Object} [options] An object literal containing key/value pairs + * to provide optional cookie attributes. + * @param {String|Number|Date} [options.expires] Either literal string to be used as cookie expires, + * or an integer specifying the expiration date from now on in days, + * or a Date object to be used as cookie expiration date. + * If a negative value is specified or a date in the past), + * the cookie will be deleted. + * If set to null or omitted, the cookie will be a session cookie + * and will not be retained when the the browser exits. + * @param {String} [options.path] Restrict access of a cookie to particular directory + * (default: path of page that created the cookie). + * @param {String} [options.domain] Override what web sites are allowed to access cookie + * (default: domain of page that created the cookie). + * @param {Boolean} [options.secure] If true, the secure attribute of the cookie will be set + * and the cookie would be accessible only from secure sites + * (cookie transmission will require secure protocol like HTTPS). + */ +function setCookie(sName, sValue, options) { + options = options || {}; + if (sValue === null) { + sValue = ''; + option.expires = 'delete'; + } + + var sCookie = sName + '=' + encodeURIComponent(sValue); + + if (options.expires) { + var oExpires = options.expires, sDate; + if (oExpires === 'delete') { + sDate = 'Thu, 01 Jan 1970 00:00:00 GMT'; + } else if (typeof oExpires === 'string') { + sDate = oExpires; + } else { + var oDate; + if (typeof oExpires === 'number') { + oDate = new Date(); + oDate.setTime(oDate.getTime() + (oExpires * 24 * 60 * 60 * 1000)); // days to ms + } else { + oDate = oExpires; + } + sDate = oDate.toGMTString(); + } + sCookie += '; expires=' + sDate; + } + + if (options.path) { + sCookie += '; path=' + (options.path); + } + if (options.domain) { + sCookie += '; domain=' + (options.domain); + } + if (options.secure) { + sCookie += '; secure'; + } + document.cookie = sCookie; +} + +/** + * Get the value of a cookie with the given name. + * + * @param {String} sName: Unique name of a cookie (letters, numbers, underscores) + * @returns {String|null} The string value stored in a cookie + */ +function getCookie(sName) { + var sRE = '(?:; )?' + sName + '=([^;]*);?'; + var oRE = new RegExp(sRE); + if (oRE.test(document.cookie)) { + return decodeURIComponent(RegExp['$1']); + } else { + return null; + } +} + +/** + * Delete cookie with given name + * + * @param {String} sName: Unique name of a cookie (letters, numbers, underscores) + * @param {Object} [options] An object literal containing key/value pairs + * to provide optional cookie attributes. + * @param {String} [options.path] Must be the same as when setting a cookie + * @param {String} [options.domain] Must be the same as when setting a cookie + */ +function deleteCookie(sName, options) { + options = options || {}; + options.expires = 'delete'; + + setCookie(sName, '', options); +} + +/* end of cookies.js */ -- cgit v1.3 From 291e52bd19877dc8fdd77079ba4c4326f114c461 Mon Sep 17 00:00:00 2001 From: John 'Warthog9' Hawley Date: Thu, 28 Apr 2011 21:04:09 +0200 Subject: gitweb: JavaScript ability to adjust time based on timezone This patch is based on Kevin Cernekee's patch series entitled "gitweb: introduce localtime feature". While Kevin's patch changed the server side output so that the timezone was output from gitweb itself, this has a number of drawbacks, in particular with respect to gitweb-caching. This patch takes the same basic goal, display the appropriate times in a given common timezone, and implements it in JavaScript. This requires adding / using a new class, "datetime", to be able to find elements to be adjusted from JavaScript. Appropriate dates are wrapped in a span with this class. Timezone to be used can be retrieved from "gitweb_tz" cookie, though currently there is no way to set / manipulate this cookie from gitweb; this is left for later commit. Valid timezones, currently, are: "utc", "local" (which means that timezone is taken from browser), and "+/-ZZZZ" numeric timezone as in RFC-2822. Default timezone is "local" (currently not configurable, left for later commit). Fallback (should JavaScript not be enabled) is to treat dates as they have been and display them, only, in UTC. Pages affected: * 'summary' view, "last change" field (commit time from latest change) * 'log' view, author time * 'commit' and 'commitdiff' views, author/committer time * 'tag' view, tagger time Based-on-code-from: Kevin Cernekee Signed-off-by: John 'Warthog9' Hawley Signed-off-by: Jakub Narebski Signed-off-by: Junio C Hamano --- gitweb/Makefile | 1 + gitweb/gitweb.perl | 11 +++++-- gitweb/static/js/adjust-timezone.js | 60 +++++++++++++++++++++++++++++++++++++ gitweb/static/js/lib/datetime.js | 15 ++++++++++ 4 files changed, 84 insertions(+), 3 deletions(-) create mode 100644 gitweb/static/js/adjust-timezone.js (limited to 'gitweb/Makefile') diff --git a/gitweb/Makefile b/gitweb/Makefile index 7dd1dee55a..5d20515fba 100644 --- a/gitweb/Makefile +++ b/gitweb/Makefile @@ -120,6 +120,7 @@ GITWEB_JSLIB_FILES += static/js/lib/common-lib.js GITWEB_JSLIB_FILES += static/js/lib/datetime.js GITWEB_JSLIB_FILES += static/js/lib/cookies.js GITWEB_JSLIB_FILES += static/js/javascript-detection.js +GITWEB_JSLIB_FILES += static/js/adjust-timezone.js GITWEB_JSLIB_FILES += static/js/blame_incremental.js diff --git a/gitweb/gitweb.perl b/gitweb/gitweb.perl index 67bcfe894e..6651946f54 100755 --- a/gitweb/gitweb.perl +++ b/gitweb/gitweb.perl @@ -3732,9 +3732,14 @@ sub git_footer_html { qq!startBlame("!. href(action=>"blame_data", -replay=>1) .qq!",\n!. qq! "!. href() .qq!");\n!. qq!\n!; - } elsif (gitweb_check_feature('javascript-actions')) { + } else { print qq!\n!; } @@ -3940,7 +3945,7 @@ sub git_print_section { sub format_timestamp_html { my $date = shift; - my $strtime = $date->{'rfc2822'}; + my $strtime = ''.$date->{'rfc2822'}.''; my $localtime_format = '(%02d:%02d %s)'; if ($date->{'hour_local'} < 6) { diff --git a/gitweb/static/js/adjust-timezone.js b/gitweb/static/js/adjust-timezone.js new file mode 100644 index 0000000000..1577d780f0 --- /dev/null +++ b/gitweb/static/js/adjust-timezone.js @@ -0,0 +1,60 @@ +// Copyright (C) 2011, John 'Warthog9' Hawley +// 2011, Jakub Narebski + +/** + * @fileOverview Manipulate dates in gitweb output, adjusting timezone + * @license GPLv2 or later + */ + +/** + * Get common timezone and adjust dates to use this common timezone. + * + * This function is called during onload event (added to window.onload). + * + * @param {String} tzDefault: default timezone, if there is no cookie + * @param {String} tzCookieName: name of cookie to store timezone + * @param {String} tzClassName: denotes elements with date to be adjusted + */ +function onloadTZSetup(tzDefault, tzCookieName, tzClassName) { + var tzCookie = getCookie(tzCookieName); + var tz = tzCookie ? tzCookie : tzDefault; + + // server-side of gitweb produces datetime in UTC, + // so if tz is 'utc' there is no need for changes + if (tz !== 'utc') { + fixDatetimeTZ(tz, tzClassName); + } +} + + +/** + * Replace RFC-2822 dates contained in SPAN elements with tzClassName + * CSS class with equivalent dates in given timezone. + * + * @param {String} tz: numeric timezone in '(-|+)HHMM' format, or 'utc', or 'local' + * @param {String} tzClassName: specifies elements to be changed + */ +function fixDatetimeTZ(tz, tzClassName) { + // sanity check, method should be ensured by common-lib.js + if (!document.getElementsByClassName) { + return; + } + + // translate to timezone in '(-|+)HHMM' format + tz = normalizeTimezoneInfo(tz); + + // NOTE: result of getElementsByClassName should probably be cached + var classesFound = document.getElementsByClassName(tzClassName, "span"); + for (var i = 0, len = classesFound.length; i < len; i++) { + var curElement = classesFound[i]; + + // we use *.firstChild.data (W3C DOM) instead of *.innerHTML + // as the latter doesn't always work everywhere in every browser + var epoch = parseRFC2822Date(curElement.firstChild.data); + var adjusted = formatDateRFC2882(epoch, tz); + + curElement.firstChild.data = adjusted; + } +} + +/* end of adjust-timezone.js */ diff --git a/gitweb/static/js/lib/datetime.js b/gitweb/static/js/lib/datetime.js index b3dcedb141..f78c60a912 100644 --- a/gitweb/static/js/lib/datetime.js +++ b/gitweb/static/js/lib/datetime.js @@ -104,6 +104,21 @@ function formatTimezoneInfo(hours, minutes, sep) { return tzSign + padLeft(hours, 2, '0') + sep + padLeft(minutes, 2, '0'); } +/** + * translate 'utc' and 'local' to numerical timezone + * @param {String} timezoneInfo: might be 'utc' or 'local' (browser) + */ +function normalizeTimezoneInfo(timezoneInfo) { + switch (timezoneInfo) { + case 'utc': + return '+0000'; + case 'local': // 'local' is browser timezone + return localTimezoneInfo(); + } + return timezoneInfo; +} + + /** * return date in local time formatted in iso-8601 like format * 'yyyy-mm-dd HH:MM:SS +/-ZZZZ' e.g. '2005-08-07 21:49:46 +0200' -- cgit v1.3