From 8426cc0182b81135ed822d1bb706f72f036e8fb6 Mon Sep 17 00:00:00 2001 From: Tomasz Sztejka Date: Tue, 2 Nov 2021 13:34:08 +0100 Subject: [PATCH 01/21] 1.Changed the way ANT build script asks JAVAC to display to user the compiler version. Was: Directly call javac. This depended on system search path settings (ie. PATH) on windows and might differ from what ANT deduced from JAVA_HOME (if present) or automatically. Is: A ant task is asked to compile "empty.java" file which contains nothing except a comment. As a side effect of compiler argument -version the version is displayed to user. 2.Added what removes the "warning: 'includeantruntime' was not set, defaulting to build.sysclasspath=last;..." ANT warning during builds (ant >1.10) This change may be incompatible with older ANT versions. I do not have environment to test it. --- build.xml | 12 +++++++++--- empty.java | 4 ++++ 2 files changed, 13 insertions(+), 3 deletions(-) create mode 100644 empty.java diff --git a/build.xml b/build.xml index afe604969..188d157e9 100644 --- a/build.xml +++ b/build.xml @@ -37,6 +37,11 @@ + + + + + JDK version: ${ant.java.version} - - - + Calling compiler to show version: + + + Java/JVM version: ${java.version} diff --git a/empty.java b/empty.java new file mode 100644 index 000000000..4da23b15c --- /dev/null +++ b/empty.java @@ -0,0 +1,4 @@ +/* + Nothing. Just to force ANT compiler to show version. + See build.xml "prepare" target. +*/ \ No newline at end of file From 81270e636923051d2ba37ee086506e43a800deef Mon Sep 17 00:00:00 2001 From: Tomasz Sztejka Date: Tue, 2 Nov 2021 13:54:23 +0100 Subject: [PATCH 02/21] Added version indicator. Possibly in wrong place since it is not clearly described. This should be restored at final deployment. --- build.moxie | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.moxie b/build.moxie index f970c3a25..41cee37b0 100644 --- a/build.moxie +++ b/build.moxie @@ -10,7 +10,7 @@ name: Gitblit description: pure Java Git solution groupId: com.gitblit artifactId: gitblit -version: 1.9.2-SNAPSHOT +version: 1.9.2-TomaszSzt.1.0 inceptionYear: 2011 # Current stable release From 3f9015a7a404e50d63716f4a4df24d2a6f06472b Mon Sep 17 00:00:00 2001 From: Tomasz Sztejka Date: Tue, 2 Nov 2021 14:28:04 +0100 Subject: [PATCH 03/21] 1.Noticed that there is no ANT target to generate javadocs for GITBLIT source code. Added "javadoc" target to build.xml. 2.Modified .gitignore to not include build docs to repository 3.Modified .gitignore to ignore standard backup files *.java~ and etc. --- .gitignore | 6 ++++ build.xml | 35 ++++++++++++++++++- .../com/gitblit/wicket/pages/CommitPage.java | 1 + 3 files changed, 41 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index e268ccbc1..f14e45efb 100644 --- a/.gitignore +++ b/.gitignore @@ -30,3 +30,9 @@ tags /**/init.lua /**/session /nbproject/private +# backup files from most of editors +*.java~ +*.html~ +*.xml~ +# Generated javadoc files for "javadoc" target. +/javadoc diff --git a/build.xml b/build.xml index 188d157e9..dce1fdb69 100644 --- a/build.xml +++ b/build.xml @@ -26,7 +26,8 @@ - + + @@ -1302,4 +1303,36 @@ GB_RELEASE_TAG=${project.tag} + + + + + + + + + + + + diff --git a/src/main/java/com/gitblit/wicket/pages/CommitPage.java b/src/main/java/com/gitblit/wicket/pages/CommitPage.java index 3998204d1..0bd988727 100644 --- a/src/main/java/com/gitblit/wicket/pages/CommitPage.java +++ b/src/main/java/com/gitblit/wicket/pages/CommitPage.java @@ -58,6 +58,7 @@ @CacheControl(LastModified.BOOT) public class CommitPage extends RepositoryPage { + public CommitPage(PageParameters params) { super(params); From 6508c64d7bd22142165f1efe37c48ccc7b3be0d6 Mon Sep 17 00:00:00 2001 From: Tomasz Sztejka Date: Tue, 2 Nov 2021 14:37:19 +0100 Subject: [PATCH 04/21] Added missing JavaDoc. Due to lack of good documentation of used library this java doc is not specific enough. --- src/main/java/com/gitblit/wicket/pages/BasePage.java | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/main/java/com/gitblit/wicket/pages/BasePage.java b/src/main/java/com/gitblit/wicket/pages/BasePage.java index 0d99f5e52..9cc84b309 100644 --- a/src/main/java/com/gitblit/wicket/pages/BasePage.java +++ b/src/main/java/com/gitblit/wicket/pages/BasePage.java @@ -69,6 +69,10 @@ import com.gitblit.wicket.GitBlitWebSession; import com.gitblit.wicket.WicketUtils; +/** + Wicket WebPage page providing common services + for GitBlit purposes. +*/ public abstract class BasePage extends SessionPage { private transient Logger logger; @@ -85,6 +89,13 @@ public BasePage(PageParameters params) { customizeHeader(); } + /** + Returns logger associated with this.getClass() + If logger is not specified creates it at first invocation. + @return a logger instance. The underlying library is not specifiying + if it may return null or not, but at least one of underlying + implementations is said to never return a null. + */ protected Logger logger() { if (logger == null) { logger = LoggerFactory.getLogger(getClass()); From cbe758f4331aee71b2ccfd421466b0bfb9e7d2a0 Mon Sep 17 00:00:00 2001 From: Tomasz Sztejka Date: Tue, 2 Nov 2021 17:23:40 +0100 Subject: [PATCH 05/21] 1.Added javadoc creation to build-xml to allow easier code maintenace. 2.Changed logging policy to following: - log4j configuration is loaded, as previously, from an internal log4j.properties file (from gitblit.jar) - THEN it is overriden from $baseFolder/log4j.properties if this file exists. - THEN web.debugMode is used to override logging level properties IF THEY ARE AT DEFAULT STATE. - THEN --dailyLogFile is used to override appenders (output for log) if in default state. - if any overrides are present loaded configuration is dumped to System.out 3.Updated comments in log4j.properties AND default.properties to inform users how to control debugging level in fain grained way. 4.Prepared to add trace level commands to CommitPage since this branch is motivated mainly due to CommitPage having problems with large repositories. --- .gitignore | 1 + build.xml | 3 +- src/main/distrib/data/defaults.properties | 24 ++- src/main/java/com/gitblit/GitBlitServer.java | 170 +++++++++++++++--- .../com/gitblit/wicket/pages/CommitPage.java | 6 +- src/main/java/log4j.properties | 38 ++-- 6 files changed, 197 insertions(+), 45 deletions(-) diff --git a/.gitignore b/.gitignore index f14e45efb..f2772ee42 100644 --- a/.gitignore +++ b/.gitignore @@ -33,6 +33,7 @@ tags # backup files from most of editors *.java~ *.html~ +*.properties~ *.xml~ # Generated javadoc files for "javadoc" target. /javadoc diff --git a/build.xml b/build.xml index dce1fdb69..e95dbf9e6 100644 --- a/build.xml +++ b/build.xml @@ -1333,6 +1333,5 @@ GB_RELEASE_TAG=${project.tag} - - + diff --git a/src/main/distrib/data/defaults.properties b/src/main/distrib/data/defaults.properties index 5dea6a0a4..b5787e1e3 100644 --- a/src/main/distrib/data/defaults.properties +++ b/src/main/distrib/data/defaults.properties @@ -1484,9 +1484,29 @@ web.binaryExtensions = 7z arc arj bin dll doc docx exe gz jar lib lzh odg odf od # SINCE 0.5.0 web.aggressiveHeapManagement = false -# Run the webapp in debug mode +# Run the webapp in debug mode. +# Allowed values are: +# false - INFO reporting level +# true - DEBUG reporting level # -# SINCE 0.5.0 +# If more precise tuning is necessary ${baseFolder}/log4j.properties file +# should be used to override default settings AND this value should be left +# as "false". +# +# To enable full detailed logging create ${baseFolder}/log4j.properties text file +# with a single line: +# +# log4j.rootLogger=ALL, S +# +# to debug to console or start server with --dailyLogFile to redirect +# logging to "${baseFolder}/log/gitblit.log" file. +# +# Alternatively a more readable debug level can be achived by: +# +# log4j.rootLogger=INFO, S +# log4j.logger.com.gitblit=ALL +# +# SINCE 1.9.2 # RESTART REQUIRED web.debugMode = false diff --git a/src/main/java/com/gitblit/GitBlitServer.java b/src/main/java/com/gitblit/GitBlitServer.java index 06000f531..11d3666cf 100644 --- a/src/main/java/com/gitblit/GitBlitServer.java +++ b/src/main/java/com/gitblit/GitBlitServer.java @@ -186,36 +186,150 @@ protected final void start(Params params) { settings = new FileSettings(params.settingsfile); } } - - if (params.dailyLogFile) { - // Configure log4j for daily log file generation - InputStream is = null; - try { - is = getClass().getResourceAsStream("/log4j.properties"); - Properties loggingProperties = new Properties(); - loggingProperties.load(is); - - loggingProperties.put("log4j.appender.R.File", new File(baseFolder, "logs/gitblit.log").getAbsolutePath()); - loggingProperties.put("log4j.rootCategory", "INFO, R"); - - if (settings.getBoolean(Keys.web.debugMode, false)) { - loggingProperties.put("log4j.logger.com.gitblit", "DEBUG"); - } - - PropertyConfigurator.configure(loggingProperties); - } catch (Exception e) { - e.printStackTrace(); - } finally { + //Note: + // The loging set-up is using quite a tricky dependency + // since (in pre 2.0 on which GitBlit depends) it is + // relying on presence of some specific implementation classes on a class path. + // + // This type of dependency results in very tricky, but totally transparent + // logging settings. The presence of slf4j "slf4j-log4j12-xxx.jar" sets up + // what type of loging service will be in use. + // + // This makes however tricky to validate what setting is really used. + // Gitblit devs assumed that log4j is used, so we need to validate it. + // + assert(LoggerFactory.getILoggerFactory() instanceof org.slf4j.impl.Log4jLoggerFactory): + "Runtime environment is not configured correctly for log4j use"; + // Originally GITBlit assumed, that debug level configuration is possible + // with "web.debugMode" option in "data/default.properties" only if server is run + // with --dailyLogFile parameter what, effectively, confused users which were running + // it with log to console. + + { + //Since we know we are using log4j we can configure it from a "hidden" file + //This file is by default in "gitblit.jar" what makes it problematic for user + //to load. Thous we will first load it from that file and THEN make an attempt + //to override/append data by loading from a baseFolder/log4j.properties + + Properties loggingProperties = new Properties(); //<--- log4j configuration will be here. + //Internally stored settings. + { + InputStream is = null; try { - if (is != null) { - is.close(); - } - } catch (IOException e) { + is = getClass().getResourceAsStream("/log4j.properties"); + assert(is!=null); //this resource is always present. + loggingProperties.load(is); + } catch (Exception e) { e.printStackTrace(); + } finally { + try { + if (is != null) { + is.close(); + } + } catch (IOException e) { + e.printStackTrace(); + } } - } - } - + }; + //Externally + boolean display_logging_configuration = false; + { + final File f = new File(params.baseFolder+"/log4j.properties"); + if (f.exists()) + { + System.out.println("Using "+f+" to configure logging"); + display_logging_configuration=true; + InputStream is = null; + try{ + try{ + is = new java.io.FileInputStream(f); + loggingProperties.load(is); + }catch(Exception e) + { + e.printStackTrace(); + }; + }finally{ + try{ + if (is!=null) is.close(); + }catch(IOException ex){ ex.printStackTrace(); }; + }; + }; + }; + //Decide on debug level overrides. + { + if (settings.getBoolean(Keys.web.debugMode, false)) + { + System.out.println("Using web.debugMode to override debug levels."); + display_logging_configuration=true; + + //user requested debugging. Check if we can override settings. + if ("INFO".equals(loggingProperties.get("log4j.logger.com.gitblit"))) + { + loggingProperties.put("log4j.logger.com.gitblit", "DEBUG"); + }; + //and we need to override root settings. We need to check if settings + //are default and override them. + { + String logger_setup = loggingProperties.getProperty("log4j.rootLogger"); + if ((logger_setup!=null) && (logger_setup.startsWith("INFO"))) + { + loggingProperties.put("log4j.rootLogger","DEBUG"+logger_setup.substring("INFO".length())); + }; + }; + } + }; + //Decide of daily file overrides. Basically we can override appender file and the appender itself. + if (params.dailyLogFile) { + System.out.println("Using --dailyLogFile to override log output."); + display_logging_configuration=true; + //We start from validating if appender selection is default. + String logger_setup = loggingProperties.getProperty("log4j.rootLogger"); + if (logger_setup!=null) + { + //We may have many appenders and only one level. + //Basically we check, if appenders specs do match defaults: + logger_setup=logger_setup.trim(); + final int n = logger_setup.length(); + if (n>=2) + { + if (( logger_setup.charAt(n-1)=='S') + && + ( + ( logger_setup.charAt(n-2)==',') + || + ( Character.isWhitespace(logger_setup.charAt(n-2))) + )) + { + //we can replace it. + loggingProperties.put("log4j.rootLogger",logger_setup.substring(0,n-1)+"R"); + //And only now if we can replace a target file. + if ("logs/gitblit.log".equals(loggingProperties.get("log4j.appender.R.File"))) + { + //yes, it does, we can override it + loggingProperties.put("log4j.appender.R.File", new File(baseFolder, "logs/gitblit.log").getAbsolutePath()); + }; + } + } + } + }; + //Now apply them to log4j. + try{ + if (display_logging_configuration) + { + System.out.println("Logging configuration data:"); + for(Object S:loggingProperties.keySet()) + { + System.out.println("\t"+S+"="+loggingProperties.get(S)); + }; + }; + PropertyConfigurator.configure(loggingProperties); + }catch(Exception ex) + { + ex.printStackTrace(); + }; + }; + + logger = LoggerFactory.getLogger(GitBlitServer.class); logger.info("\n" + Constants.getASCIIArt()); @@ -224,7 +338,7 @@ protected final void start(Params params) { String osname = System.getProperty("os.name"); String osversion = System.getProperty("os.version"); logger.info("Running on " + osname + " (" + osversion + ")"); - + String javaversion = System.getProperty("java.version"); String javavendor = System.getProperty("java.vendor"); logger.info("JVM version " + javaversion + " (" + javavendor + ")"); diff --git a/src/main/java/com/gitblit/wicket/pages/CommitPage.java b/src/main/java/com/gitblit/wicket/pages/CommitPage.java index 0bd988727..118c718b2 100644 --- a/src/main/java/com/gitblit/wicket/pages/CommitPage.java +++ b/src/main/java/com/gitblit/wicket/pages/CommitPage.java @@ -58,12 +58,16 @@ @CacheControl(LastModified.BOOT) public class CommitPage extends RepositoryPage { - + public CommitPage(PageParameters params) { super(params); + + Repository r = getRepository(); RevCommit c = getCommit(); + + logger().trace("Building commits page for:"+r+" commit "+c); List parents = new ArrayList(); if (c.getParentCount() > 0) { diff --git a/src/main/java/log4j.properties b/src/main/java/log4j.properties index 43d31d80b..d28bbfe5a 100644 --- a/src/main/java/log4j.properties +++ b/src/main/java/log4j.properties @@ -5,26 +5,36 @@ # appenders. For the console, use 'S'. For the daily rolling file, use 'R'. # For an HTML formatted log, use 'H'. # -# To override the default (rootCategory) log level, define a property of the +# To override the default (rootLogger) log level, define a property of the # form (see below for available values): # -# log4j.logger. = +# log4j.logger.xxx = # -# Available logger names: -# TODO # -# Possible Log Levels: -# FATAL, ERROR, WARN, INFO, DEBUG +# Possible Log Levels, from more serious to less serious +# OFF, FATAL, ERROR, WARN, INFO, DEBUG, ALL # #------------------------------------------------------------------------------ -log4j.rootCategory=INFO, S -#log4j.rootLogger=INFO -#log4j.logger.org=INFO -#log4j.logger.com=INFO -#log4j.logger.net=INFO +# Set up main level filtering: +# log4j.rootLogger=, +# where: +# - any log event which is at least that serious will be logged, ie. +# +# This value will be overriden to "DEBUG" if server is configured with web.debugMode==true +# in "data/default.properties" file. +# +# - one of defined below log4j.appender. "appenders" +# which do decide where the logs will be stored/printed to. +# +# This value will be overriden to R if server is run with --dailyLogFile +# +# Default: log4j.rootLogger=INFO, S +# Log importance "Info" to "console". +# +log4j.rootLogger=INFO, S -#log4j.logger.com.gitblit=DEBUG +log4j.logger.com.gitblit=INFO log4j.logger.com.gitblit.transport.ssh.SshServerSession=WARN log4j.logger.org.apache.sshd=WARN log4j.logger.org.apache.mina=WARN @@ -50,6 +60,10 @@ log4j.appender.S.layout.ConversionPattern = %d{yyyy-MM-dd HH:mm:ss} [%-5p] %m%n # #------------------------------------------------------------------------------ log4j.appender.R = org.apache.log4j.DailyRollingFileAppender +# If below value is: +# logs/gitblit.log +# it will be overrided to $baseFolder/logs/gitblit.log +# if server is started with --dailyLogFile option. log4j.appender.R.File = logs/gitblit.log log4j.appender.R.Append = true log4j.appender.R.DatePattern = '.'yyyy-MM-dd From 5b9df26652b3ad853e1f5c6f3eea78f06d315dc3 Mon Sep 17 00:00:00 2001 From: Tomasz Sztejka Date: Wed, 3 Nov 2021 10:30:31 +0100 Subject: [PATCH 06/21] 1.Added some notes to build.xml to inform users about - potential environment variables needed - SSL conflict with pre 7.171 JRE version used to run ANT 2.Added Windows script setEnv.bat which explains what needs to be set up and how to let ANT 1.10 or ANT 1.9 to use more modern JRE to avoid SSL exceptions and still compile using JDK 1.7. --- build.xml | 18 +++++++++++++----- setEnv.bat | 37 +++++++++++++++++++++++++++++++++++++ 2 files changed, 50 insertions(+), 5 deletions(-) create mode 100644 setEnv.bat diff --git a/build.xml b/build.xml index e95dbf9e6..d4f0160da 100644 --- a/build.xml +++ b/build.xml @@ -1,6 +1,17 @@ - + Date: Wed, 3 Nov 2021 11:15:54 +0100 Subject: [PATCH 07/21] 1.Finished GitBlitServer code responsible for full blown logging set-up. 2.Added example log setup file with excessive manual inside. 3.Added some notes to build file how to boost compilation speed which with moxie is horrible when compared to pure ANT solution. It basically repetively tries do download everything and compile from scratch. We have "clean" target to enforce it, don't we? Plain compile should be hell fast. --- build.xml | 6 +- .../data/log4j-debug-example.properties | 59 ++++ src/main/java/com/gitblit/GitBlitServer.java | 319 ++++++++++-------- src/main/java/log4j.properties | 8 + 4 files changed, 246 insertions(+), 146 deletions(-) create mode 100644 src/main/distrib/data/log4j-debug-example.properties diff --git a/build.xml b/build.xml index d4f0160da..731e8b76c 100644 --- a/build.xml +++ b/build.xml @@ -151,7 +151,8 @@ - + @@ -1103,7 +1104,8 @@ GB_RELEASE_TAG=${project.tag} - + + diff --git a/src/main/distrib/data/log4j-debug-example.properties b/src/main/distrib/data/log4j-debug-example.properties new file mode 100644 index 000000000..1a60c4354 --- /dev/null +++ b/src/main/distrib/data/log4j-debug-example.properties @@ -0,0 +1,59 @@ +# +# In this example we show how to override +# debugging settings to provide detailed information +# about what and where have happen in GIT blit +# +# Settings made with this file do CONFLICT with --dailyLogFile command line +# and web.debugMode option overriding their effects. +# +# To activate this example rename it to "log4j.properties" +# and restart server. +# + +# +# In below example we are saying that globally we are interested +# in messages on the level INFO and are going to output it to +# console. +# +# If output to a file is expected use: +# +# log4j.rootLogger=INFO, R +# +# Note: The --dailyLogFile server command line parameter is an +# another option to output to a file, but +# we should not use it with conjunction with this settings. +# +# Other levels You may use are ERROR,WARN,INFO,DEBUG,ALL, from most important +# (ERROR) to less (ALL). +# +log4j.rootLogger=INFO, S + +# +# In this line we are telling that all JAVA classes which come from +# any com.gitblit.* package should report everything they can do. +# +log4j.logger.com.gitblit=ALL +# +# Now, since we are doing a bug hunt we do enable +# full blow formatting which will tell us not only a message, but also where +# exactly it was made in code. Notice, this hurts performance. + +# +# Below line ensures, that specified layout is used for file storage +# +log4j.appender.R.layout = org.apache.log4j.PatternLayout +# +# And what exactly the layout is. The %d means the date of message, +# with up to 1ms accuracy. +# The %l means source information +# %p means priority (the -5) means "left pad with spaces up to 5 chars long +# %m means "message" +# %n means "end of line" +# +log4j.appender.R.layout.ConversionPattern = %d{yyyy-MM-dd HH:mm:ss.SSS} [%-5p] %n %l %n %m%n + +# +# We do repeat it for console output. +# +log4j.appender.S.layout = org.apache.log4j.PatternLayout +log4j.appender.S.layout.ConversionPattern = %d{yyyy-MM-dd HH:mm:ss.SSS} %l [%-5p] %n %l %n %m%n \ No newline at end of file diff --git a/src/main/java/com/gitblit/GitBlitServer.java b/src/main/java/com/gitblit/GitBlitServer.java index 11d3666cf..1a72cbf48 100644 --- a/src/main/java/com/gitblit/GitBlitServer.java +++ b/src/main/java/com/gitblit/GitBlitServer.java @@ -73,7 +73,7 @@ /** * GitBlitServer is the embedded Jetty server for Gitblit GO. This class starts * and stops an instance of Jetty that is configured from a combination of the - * gitblit.properties file and command line parameters. JCommander is used to + * gitblit.properties file and command line parameters. Args4j is used to * simplify command line parameter processing. This class also automatically * generates a self-signed certificate for localhost, if the keystore does not * already exist. @@ -186,149 +186,8 @@ protected final void start(Params params) { settings = new FileSettings(params.settingsfile); } } - //Note: - // The loging set-up is using quite a tricky dependency - // since (in pre 2.0 on which GitBlit depends) it is - // relying on presence of some specific implementation classes on a class path. - // - // This type of dependency results in very tricky, but totally transparent - // logging settings. The presence of slf4j "slf4j-log4j12-xxx.jar" sets up - // what type of loging service will be in use. - // - // This makes however tricky to validate what setting is really used. - // Gitblit devs assumed that log4j is used, so we need to validate it. - // - assert(LoggerFactory.getILoggerFactory() instanceof org.slf4j.impl.Log4jLoggerFactory): - "Runtime environment is not configured correctly for log4j use"; - // Originally GITBlit assumed, that debug level configuration is possible - // with "web.debugMode" option in "data/default.properties" only if server is run - // with --dailyLogFile parameter what, effectively, confused users which were running - // it with log to console. - - { - //Since we know we are using log4j we can configure it from a "hidden" file - //This file is by default in "gitblit.jar" what makes it problematic for user - //to load. Thous we will first load it from that file and THEN make an attempt - //to override/append data by loading from a baseFolder/log4j.properties - - Properties loggingProperties = new Properties(); //<--- log4j configuration will be here. - //Internally stored settings. - { - InputStream is = null; - try { - is = getClass().getResourceAsStream("/log4j.properties"); - assert(is!=null); //this resource is always present. - loggingProperties.load(is); - } catch (Exception e) { - e.printStackTrace(); - } finally { - try { - if (is != null) { - is.close(); - } - } catch (IOException e) { - e.printStackTrace(); - } - } - }; - //Externally - boolean display_logging_configuration = false; - { - final File f = new File(params.baseFolder+"/log4j.properties"); - if (f.exists()) - { - System.out.println("Using "+f+" to configure logging"); - display_logging_configuration=true; - InputStream is = null; - try{ - try{ - is = new java.io.FileInputStream(f); - loggingProperties.load(is); - }catch(Exception e) - { - e.printStackTrace(); - }; - }finally{ - try{ - if (is!=null) is.close(); - }catch(IOException ex){ ex.printStackTrace(); }; - }; - }; - }; - //Decide on debug level overrides. - { - if (settings.getBoolean(Keys.web.debugMode, false)) - { - System.out.println("Using web.debugMode to override debug levels."); - display_logging_configuration=true; - - //user requested debugging. Check if we can override settings. - if ("INFO".equals(loggingProperties.get("log4j.logger.com.gitblit"))) - { - loggingProperties.put("log4j.logger.com.gitblit", "DEBUG"); - }; - //and we need to override root settings. We need to check if settings - //are default and override them. - { - String logger_setup = loggingProperties.getProperty("log4j.rootLogger"); - if ((logger_setup!=null) && (logger_setup.startsWith("INFO"))) - { - loggingProperties.put("log4j.rootLogger","DEBUG"+logger_setup.substring("INFO".length())); - }; - }; - } - }; - //Decide of daily file overrides. Basically we can override appender file and the appender itself. - if (params.dailyLogFile) { - System.out.println("Using --dailyLogFile to override log output."); - display_logging_configuration=true; - //We start from validating if appender selection is default. - String logger_setup = loggingProperties.getProperty("log4j.rootLogger"); - if (logger_setup!=null) - { - //We may have many appenders and only one level. - //Basically we check, if appenders specs do match defaults: - logger_setup=logger_setup.trim(); - final int n = logger_setup.length(); - if (n>=2) - { - if (( logger_setup.charAt(n-1)=='S') - && - ( - ( logger_setup.charAt(n-2)==',') - || - ( Character.isWhitespace(logger_setup.charAt(n-2))) - )) - { - //we can replace it. - loggingProperties.put("log4j.rootLogger",logger_setup.substring(0,n-1)+"R"); - //And only now if we can replace a target file. - if ("logs/gitblit.log".equals(loggingProperties.get("log4j.appender.R.File"))) - { - //yes, it does, we can override it - loggingProperties.put("log4j.appender.R.File", new File(baseFolder, "logs/gitblit.log").getAbsolutePath()); - }; - } - } - } - }; - //Now apply them to log4j. - try{ - if (display_logging_configuration) - { - System.out.println("Logging configuration data:"); - for(Object S:loggingProperties.keySet()) - { - System.out.println("\t"+S+"="+loggingProperties.get(S)); - }; - }; - PropertyConfigurator.configure(loggingProperties); - }catch(Exception ex) - { - ex.printStackTrace(); - }; - }; - + /* -------- set up logging ---------*/ + setUpLogging(settings,params); logger = LoggerFactory.getLogger(GitBlitServer.class); logger.info("\n" + Constants.getASCIIArt()); @@ -597,6 +456,178 @@ private boolean isWindows() { return System.getProperty("os.name").toLowerCase().indexOf("windows") > -1; } + /** + A routine responsible for setting up log4j/slf4j logging + environment. This method should be called inside {@link #start} + before {@link #logger} is initialized. +

+ If {@link #logger} is already initialized this method does not do + anything. +

+ This method is using data stored in: +

    +
  • /log4j.properties in jar properties file;
  • +
  • /log4j.properties stored in base folder + as an override to internal jar properties file;
  • +
  • web.debugMode setting in defaults.properties + and gitblit.properties files;
  • +
  • --dailyLogFile command line option.
  • +
+ All errors are printed to System.err and ignored. + If any override of jar /log4j.properties is applied + apropriate informations are printed to System.out. + @param settings content of "default.properties" file and/or it's overrides. + @param params parsed command-line parameters + */ + private void setUpLogging(final FileSettings settings, final Params params) + { + final File baseFolder = getBaseFolder(params); + //Note: + // The loging set-up is using quite a tricky dependency + // since slf4j (in pre 2.0 on which GitBlit depends) is + // relying on presence of some specific implementation classes on a class path. + // + // This type of dependency results in invisible in code + // but totally transparent logging engine selection. + // The presence of slf4j "slf4j-log4j12-xxx.jar" sets up + // what type of loging service will be in use. + // + // This makes however tricky to validate what setting is really used. + // Gitblit devs assumed that log4j is used, so we need to validate it. + // + assert(LoggerFactory.getILoggerFactory() instanceof org.slf4j.impl.Log4jLoggerFactory): + "Runtime environment is not configured correctly for log4j use"; + + // Originally GITBlit assumed, that debug level configuration is possible + // with "web.debugMode" option in "data/default.properties" only if server is run + // with --dailyLogFile parameter what, effectively, confused users which were running + // it with log to console. + + //Since we know we are using log4j we can configure it from a "hidden" file + //This file is by default in "gitblit.jar" what makes it problematic for user + //to load. Thous we will first load it from that file and THEN make an attempt + //to override/append data by loading from a baseFolder/log4j.properties + + Properties loggingProperties = new Properties(); //<--- log4j configuration will be here. + //Internally stored settings. + { + InputStream is = null; + try{ + is = getClass().getResourceAsStream("/log4j.properties"); + assert(is!=null); //this resource is always present. + loggingProperties.load(is); + }catch (Exception e) + { + e.printStackTrace(); + System.err.println("Could not set up logging services. Logging services are running in default mode."); + return; //Nothing else to do + }finally + { + try{ + if (is != null) is.close(); + } catch (IOException e) { + e.printStackTrace(); + //This exception should not prevent us from continouing. + } + } + }; + //Now pick up default setting so we could later detect if user have overriden it. + //Both defaults cannot be null. Since this is an internal file assertions are fine. + final String default_gitblit_level = loggingProperties.getProperty("log4j.logger.com.gitblit"); + assert(default_gitblit_level!=null):"invalid content of jar stored log4j.properties file, missing log4j.logger.com.gitblit"; + final String default_root_level = loggingProperties.getProperty("log4j.rootLogger"); + assert(default_root_level!=null):"invalid content of jar stored log4j.properties file, missing log4j.rootLogger"; + final String default_log_file = loggingProperties.getProperty("log4j.appender.R.File"); + assert(default_log_file!=null):"invalid content of jar stored log4j.properties file, missing log4j.appender.R.File"; + + //Load overrides from file + boolean display_logging_configuration = false; //<-- this variable will keep track if any override is + //applied so we can later show overrides to user. + { + final File f = new File(params.baseFolder+"/log4j.properties"); + if (f.exists()) + { + display_logging_configuration=true; + System.out.println("Using "+f+" to override logging configuration."); + InputStream is = null; + try{ + try{ + is = new java.io.FileInputStream(f); + loggingProperties.load(is); + }catch(Exception e) + { + System.err.println("Failed to load user defined logging settings due to "+e); + e.printStackTrace(); + //this does not stop us from continuing. + }; + }finally{ + try{ + if (is!=null) is.close(); + }catch(IOException ex){ ex.printStackTrace(); }; //<-- again we can continue + }; + }; + }; + //Now apply overrides to level controlled by web.debugMode option. + //These overrides are applied ONLY if default level setting were not overriden + //by custom log4j.properties file. + if (settings.getBoolean(Keys.web.debugMode, false)) + { + display_logging_configuration=true; + System.out.println("Using web.debugMode to override debug levels."); + if (default_gitblit_level.equals(loggingProperties.getProperty("log4j.logger.com.gitblit"))) + { + loggingProperties.setProperty("log4j.logger.com.gitblit", "DEBUG"); + }else + System.out.println("web.debugMode requested, but ignored due to log4j.properties file was used to override log4j.logger.com.gitblit setup"); + if (default_root_level.equals(loggingProperties.getProperty("log4j.rootLogger"))) + { + //Now the override needs to be partial only, + int i = default_root_level.indexOf(','); + assert(i!=-1):"internal log4j.properties log4j.rootLogger="+default_root_level+", missing default appender."; + loggingProperties.setProperty("log4j.rootLogger", "DEBUG"+default_root_level.substring(i)); + }else + System.out.println("web.debugMode requested, but ignored due to log4j.properties file was used to override log4j.rootLogger setup"); + } + //Decide of daily file overrides. Basically we can override appender file and the appender itself. + if (params.dailyLogFile) + { + display_logging_configuration=true; + System.out.println("Using --dailyLogFile to override log output."); + //We start from validating if appender selection is default. + if (default_root_level.equals(loggingProperties.getProperty("log4j.rootLogger"))) + { + int i = default_root_level.indexOf(','); + assert(i!=-1):"internal log4j.properties log4j.rootLogger="+default_root_level+", missing default appender."; + loggingProperties.setProperty("log4j.rootLogger", default_root_level.substring(0,i)+",R"); + }else + System.out.println("--dailyLogFile requested, but ignored due to log4j.properties file was used to override log4j.rootLogger setup"); + //And now check if we can replace a target file. + if (default_log_file.equals(loggingProperties.get("log4j.appender.R.File"))) + { + //yes, it does, we can override it + loggingProperties.setProperty("log4j.appender.R.File", new File(baseFolder, default_log_file).getAbsolutePath()); + }else + System.out.println("--dailyLogFile requested, but ignored due to log4j.properties file was used to override log4j.appender.R.File setup"); + }; + //All overrides are applied. If any overrides were requested we need to show them since due to multiple + //ways of defining configurations it may be tricky. + if (display_logging_configuration) + { + System.out.println("Combined logging setup data are:"); + for(Object S:loggingProperties.keySet()) + { + System.out.println("\t"+S+"="+loggingProperties.get(S)); + }; + }; + //Now apply them to log4j. + try{ + PropertyConfigurator.configure(loggingProperties); + }catch(Exception ex) + { + System.err.println("Failed to apply log4j configuration due to "+ex); + ex.printStackTrace(); + }; + }; /** * The ShutdownMonitorThread opens a socket on a specified port and waits * for an incoming connection. When that connection is accepted a shutdown diff --git a/src/main/java/log4j.properties b/src/main/java/log4j.properties index d28bbfe5a..74b1854a9 100644 --- a/src/main/java/log4j.properties +++ b/src/main/java/log4j.properties @@ -14,6 +14,7 @@ # Possible Log Levels, from more serious to less serious # OFF, FATAL, ERROR, WARN, INFO, DEBUG, ALL # +# #------------------------------------------------------------------------------ # Set up main level filtering: @@ -32,8 +33,13 @@ # Default: log4j.rootLogger=INFO, S # Log importance "Info" to "console". # + +# Note: change in this option should be consulted with GitBlitServer.java +# Note: this optin may be overrided with web.debugMode log4j.rootLogger=INFO, S +# Note: change in this option should be consulted with GitBlitServer.java +# Note: this optin may be overrided with web.debugMode log4j.logger.com.gitblit=INFO log4j.logger.com.gitblit.transport.ssh.SshServerSession=WARN log4j.logger.org.apache.sshd=WARN @@ -49,6 +55,7 @@ log4j.logger.org.apache.wicket.protocol.http.HttpSessionStore=WARN # See http://logging.apache.org/log4j/docs/api/index.html for details. # #------------------------------------------------------------------------------ +# Note: change in this option should be consulted with GitBlitServer.java log4j.appender.S = org.apache.log4j.ConsoleAppender log4j.appender.S.layout = org.apache.log4j.PatternLayout log4j.appender.S.layout.ConversionPattern = %d{yyyy-MM-dd HH:mm:ss} [%-5p] %m%n @@ -59,6 +66,7 @@ log4j.appender.S.layout.ConversionPattern = %d{yyyy-MM-dd HH:mm:ss} [%-5p] %m%n # See http://logging.apache.org/log4j/docs/api/index.html for details. # #------------------------------------------------------------------------------ +# Note: change in this option should be consulted with GitBlitServer.java log4j.appender.R = org.apache.log4j.DailyRollingFileAppender # If below value is: # logs/gitblit.log From ffb83de8ef4c4c2546b77e89c24b41d089e73685 Mon Sep 17 00:00:00 2001 From: Tomasz Sztejka Date: Wed, 3 Nov 2021 11:51:37 +0100 Subject: [PATCH 08/21] Non Compiling. 1.Detected that RepositoryPage is using logger field which duplicates logger() call from superclass BasePage. Since logger() is lazy initialized I did remove the field. Subsequent commit will be used to detect and re-factor it to use base class logger() call. --- .../java/com/gitblit/wicket/pages/RepositoryPage.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/main/java/com/gitblit/wicket/pages/RepositoryPage.java b/src/main/java/com/gitblit/wicket/pages/RepositoryPage.java index 36c5ae16c..03d6d5584 100644 --- a/src/main/java/com/gitblit/wicket/pages/RepositoryPage.java +++ b/src/main/java/com/gitblit/wicket/pages/RepositoryPage.java @@ -78,7 +78,7 @@ public abstract class RepositoryPage extends RootPage { - protected final Logger logger = LoggerFactory.getLogger(getClass()); + //protected final Logger logger = LoggerFactory.getLogger(getClass()); private final String PARAM_STAR = "star"; @@ -233,13 +233,13 @@ private List registerNavLinks() { navLinks.add(new PageNavLink("gb.tree", TreePage.class, objectParams)); if (app().tickets().isReady() && (app().tickets().isAcceptingNewTickets(model) || app().tickets().hasTickets(model))) { PageParameters tParams = WicketUtils.newOpenTicketsParameter(getRepositoryName()); - navLinks.add(new PageNavLink("gb.tickets", TicketsPage.class, tParams)); - } + navLinks.add(new PageNavLink("gb.tickets", TicketsPage.class, tParams)); + } navLinks.add(new PageNavLink("gb.docs", DocsPage.class, objectParams, true)); if (app().settings().getBoolean(Keys.web.allowForking, true)) { navLinks.add(new PageNavLink("gb.forks", ForksPage.class, params, true)); } - navLinks.add(new PageNavLink("gb.compare", ComparePage.class, params, true)); + navLinks.add(new PageNavLink("gb.compare", ComparePage.class, params, true)); // conditional links // per-repository extra navlinks From b7dc1fc485e877cad195b92a3cdaa50b88be34ac Mon Sep 17 00:00:00 2001 From: Tomasz Sztejka Date: Wed, 3 Nov 2021 11:57:08 +0100 Subject: [PATCH 09/21] Compiling. Finished refactoring from logger field to logger() call. --- src/main/java/com/gitblit/wicket/pages/BlamePage.java | 2 +- src/main/java/com/gitblit/wicket/pages/EditFilePage.java | 2 +- src/main/java/com/gitblit/wicket/pages/MetricsPage.java | 2 +- src/main/java/com/gitblit/wicket/pages/RepositoryPage.java | 2 +- src/main/java/com/gitblit/wicket/pages/SummaryPage.java | 2 +- src/main/java/com/gitblit/wicket/pages/TicketPage.java | 6 +++--- 6 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/main/java/com/gitblit/wicket/pages/BlamePage.java b/src/main/java/com/gitblit/wicket/pages/BlamePage.java index 2fcca0ae1..8465058f3 100644 --- a/src/main/java/com/gitblit/wicket/pages/BlamePage.java +++ b/src/main/java/com/gitblit/wicket/pages/BlamePage.java @@ -108,7 +108,7 @@ public BlamePage(PageParameters params) { if (pathModel == null) { final String notFound = MessageFormat.format("Blame page failed to find {0} in {1} @ {2}", blobPath, repositoryName, objectId); - logger.error(notFound); + logger().error(notFound); add(new Label("annotation").setVisible(false)); add(new Label("missingBlob", missingBlob(blobPath, commit)).setEscapeModelStrings(false)); return; diff --git a/src/main/java/com/gitblit/wicket/pages/EditFilePage.java b/src/main/java/com/gitblit/wicket/pages/EditFilePage.java index dbf8a79e2..5ec9b6ee8 100644 --- a/src/main/java/com/gitblit/wicket/pages/EditFilePage.java +++ b/src/main/java/com/gitblit/wicket/pages/EditFilePage.java @@ -123,7 +123,7 @@ protected void onSubmit() { try { ObjectId docAtLoad = getRepository().resolve(commitIdAtLoad.getObject()); - logger.trace("Commiting Edit File page: " + commitIdAtLoad.getObject()); + logger().trace("Commiting Edit File page: " + commitIdAtLoad.getObject()); DirCache index = DirCache.newInCore(); DirCacheBuilder builder = index.builder(); diff --git a/src/main/java/com/gitblit/wicket/pages/MetricsPage.java b/src/main/java/com/gitblit/wicket/pages/MetricsPage.java index 96113b0f6..77811687a 100644 --- a/src/main/java/com/gitblit/wicket/pages/MetricsPage.java +++ b/src/main/java/com/gitblit/wicket/pages/MetricsPage.java @@ -94,7 +94,7 @@ private void createLineChart(Charts charts, String id, List metrics) { try { date = df.parse(metric.name); } catch (ParseException e) { - logger.error("Unable to parse date: " + metric.name); + logger().error("Unable to parse date: " + metric.name); return; } chart.addValue(date, (int)metric.count); diff --git a/src/main/java/com/gitblit/wicket/pages/RepositoryPage.java b/src/main/java/com/gitblit/wicket/pages/RepositoryPage.java index 03d6d5584..1f9d64e74 100644 --- a/src/main/java/com/gitblit/wicket/pages/RepositoryPage.java +++ b/src/main/java/com/gitblit/wicket/pages/RepositoryPage.java @@ -144,7 +144,7 @@ public RepositoryPage(PageParameters params) { try { app().gitblit().reviseUser(user.username, user); } catch (GitBlitException e) { - logger.error("Failed to update user " + user.username, e); + logger().error("Failed to update user " + user.username, e); error(getString("gb.failedToUpdateUser"), false); } } diff --git a/src/main/java/com/gitblit/wicket/pages/SummaryPage.java b/src/main/java/com/gitblit/wicket/pages/SummaryPage.java index 3cfa152e8..1a5f05182 100644 --- a/src/main/java/com/gitblit/wicket/pages/SummaryPage.java +++ b/src/main/java/com/gitblit/wicket/pages/SummaryPage.java @@ -190,7 +190,7 @@ private Charts createCharts(List metrics) { try { date = df.parse(metric.name); } catch (ParseException e) { - logger.error("Unable to parse date: " + metric.name); + logger().error("Unable to parse date: " + metric.name); return charts; } chart.addValue(date, (int)metric.count); diff --git a/src/main/java/com/gitblit/wicket/pages/TicketPage.java b/src/main/java/com/gitblit/wicket/pages/TicketPage.java index e2133966a..d212e3be6 100644 --- a/src/main/java/com/gitblit/wicket/pages/TicketPage.java +++ b/src/main/java/com/gitblit/wicket/pages/TicketPage.java @@ -1451,7 +1451,7 @@ public void run() { } else { // merge failure String msg = MessageFormat.format("Failed to merge ticket {0,number,0}: {1}", ticket.number, result.name()); - logger.error(msg); + logger().error(msg); GitBlitWebSession.get().cacheErrorMessage(msg); } } @@ -1461,13 +1461,13 @@ public void run() { String msg = MessageFormat.format("Can not merge ticket {0,number,0}, patchset {1,number,0} has been vetoed!", ticket.number, patchset.number); GitBlitWebSession.get().cacheErrorMessage(msg); - logger.error(msg); + logger().error(msg); } } else { // not current patchset String msg = MessageFormat.format("Can not merge ticket {0,number,0}, the patchset has been updated!", ticket.number); GitBlitWebSession.get().cacheErrorMessage(msg); - logger.error(msg); + logger().error(msg); } redirectTo(TicketsPage.class, getPageParameters()); From 375b4fc7005bdd1c3866e72bc91a18c7331679a8 Mon Sep 17 00:00:00 2001 From: Tomasz Sztejka Date: Wed, 3 Nov 2021 12:12:13 +0100 Subject: [PATCH 10/21] 1.Added some comments about multithread safety in BasePage javadocs. I think it is important to not need to dig into Wicket docs to figure it out. 2.Changed javadoc target to chew into dependencies to correctly display all implemented interfaces. --- build.xml | 5 +++-- .../com/gitblit/wicket/pages/SessionPage.java | 21 +++++++++++++++++++ 2 files changed, 24 insertions(+), 2 deletions(-) diff --git a/build.xml b/build.xml index 731e8b76c..65bc9c73c 100644 --- a/build.xml +++ b/build.xml @@ -151,8 +151,8 @@ - + @@ -1330,6 +1330,7 @@ GB_RELEASE_TAG=${project.tag} maxmemory="480m" sourcepath="${project.src.dir}" destdir="${project.src.javadoc.dir}" + classpath="${basedir}/ext/*;${basedir}/build/classes" additionalparam=" -version" linksource="yes" includenosourcepackages="yes" diff --git a/src/main/java/com/gitblit/wicket/pages/SessionPage.java b/src/main/java/com/gitblit/wicket/pages/SessionPage.java index d715aaeef..5f387fe86 100644 --- a/src/main/java/com/gitblit/wicket/pages/SessionPage.java +++ b/src/main/java/com/gitblit/wicket/pages/SessionPage.java @@ -31,6 +31,27 @@ import com.gitblit.wicket.GitBlitWebApp; import com.gitblit.wicket.GitBlitWebSession; +/** + A base of "wicket" web pages. + +

Thread safety

+ As it seems that wickets ensure that page exists + only during a duration of a session and is handled + in only one thread no thread safety measures are + necessay since page instance code is never run concurrently. +

+ + Is saying: +

    +
  • Never share component object instances, models, + and behaviors between pages that are in several page maps. + Although the chance that a user will trigger two pages in different + page maps at the same time is slight, it’s possible, + especially with pages that take a while to render.
  • +
  • Application objects, session objects, and session stores aren’t + thread-safe.
  • +
+*/ public abstract class SessionPage extends WebPage { public SessionPage() { From 82ee8ee47531d6275b0378b8c1d55b582a2c6c1f Mon Sep 17 00:00:00 2001 From: Tomasz Sztejka Date: Wed, 3 Nov 2021 12:35:50 +0100 Subject: [PATCH 11/21] 1.Updated some comments in BasePage according to logging. 2.Added fast access checks if log levels are enabled. 3.Provided recommended use patterns in javadocs. --- .../com/gitblit/wicket/pages/BasePage.java | 148 +++++++++++++++++- 1 file changed, 144 insertions(+), 4 deletions(-) diff --git a/src/main/java/com/gitblit/wicket/pages/BasePage.java b/src/main/java/com/gitblit/wicket/pages/BasePage.java index 9cc84b309..85d552290 100644 --- a/src/main/java/com/gitblit/wicket/pages/BasePage.java +++ b/src/main/java/com/gitblit/wicket/pages/BasePage.java @@ -75,8 +75,26 @@ */ public abstract class BasePage extends SessionPage { - private transient Logger logger; - + /** Lazy initialized with {@link #logger} */ + private transient Logger logger; //Note: this class could be static final which would be + //possibly faster and better, but might get into + //initialization ordering problem since GitBlitServer + //do re-configure logs in constructor, rather than + //in static constructor. The entire logger facility could + //be the faster, since JIT could optimize out const calls. + /** Keeps TRACE logging status. Set up in {@link #getLogger} + first call. Zero means "unknown", "1" disabled and "2" enabled */ + private transient byte is_Trace_Enabled; + /** See {@link #is_Trace_Enabled} */ + private transient byte is_Debug_Enabled; + /** See {@link #is_Trace_Enabled} */ + private transient byte is_Info_Enabled; + /** See {@link #is_Trace_Enabled} */ + private transient byte is_Warn_Enabled; + /** See {@link #is_Trace_Enabled} */ + private transient byte is_Error_Enabled; + + /** Lazy initialized with {@link #getTimeUtils} */ private transient TimeUtils timeUtils; public BasePage() { @@ -91,17 +109,139 @@ public BasePage(PageParameters params) { /** Returns logger associated with this.getClass() - If logger is not specified creates it at first invocation. + If logger is not already present it creates it at first invocation. +

+ Notes about logging performance. +

+ Use below pattern for WARN and ERROR levels. +

+			logger().warn("message");
+			logger().error("message");
+		
+

+ Logging at level WARN or ERROR is usually rare and even a costly + process of formulating text does not impact performance. + Logging at levels TRACE, DEBUG, and INFO are few order of magnitudes + denser and especially trace may harm performance. +

+ To maximize the performance creation of log message must be avoided + if log level is not enabled. User may use: +

+			if (logger().isTraceEnabled()) logger().trace("xxx");
+		
+ but this will cost at least a sequence of 5 interface calls which + are not very fast. +

+ To avoid this burden and ecourage users to use extensive logging + this class is providing own {@link #isTraceEnabled}, + {@link #isDebugEnabled}, {@link #isInfoEnabled}, {@link #isWarnEnabled}, + {@link #isErrorEnabled} methods which are effectively a single field read. +

+ Following pattern is recommended: +

+			if (isTraceEnable()) logger().trace(complex message);
+		
+ @return a logger instance. The underlying library is not specifiying if it may return null or not, but at least one of underlying - implementations is said to never return a null. + implementations is said to never return a null. Life time constant. */ protected Logger logger() { if (logger == null) { logger = LoggerFactory.getLogger(getClass()); + assert(logger!=null); + is_Trace_Enabled = logger.isTraceEnabled() ? (byte)2 : (byte)1; + is_Debug_Enabled = logger.isDebugEnabled() ? (byte)2 : (byte)1; + is_Info_Enabled = logger.isInfoEnabled() ? (byte)2 : (byte)1; + is_Warn_Enabled = logger.isWarnEnabled() ? (byte)2 : (byte)1; + is_Error_Enabled = logger.isErrorEnabled() ? (byte)2 : (byte)1; } return logger; } + /** A fastest possible test if TRACE logging is enabled + @return true if enable, false if not. Life time constant. + @see #logger + */ + protected final boolean isTraceEnabled() + { + switch(is_Trace_Enabled) + { + case 1: return false; + case 2: return true; + default: + logger(); + assert( (is_Trace_Enabled==1)||(is_Trace_Enabled==2)); + return isTraceEnabled(); + } + }; + + /** A fastest possible test if Debug logging is enabled + @return true if enable, false if not. Life time constant. + @see #logger + */ + protected final boolean isDebugEnabled() + { + switch(is_Debug_Enabled) + { + case 1: return false; + case 2: return true; + default: + logger(); + assert( (is_Debug_Enabled==1)||(is_Debug_Enabled==2)); + return isDebugEnabled(); + } + }; + + /** A fastest possible test if Info logging is enabled + @return true if enable, false if not. Life time constant. + @see #logger + */ + protected final boolean isInfoEnabled() + { + switch(is_Info_Enabled) + { + case 1: return false; + case 2: return true; + default: + logger(); + assert( (is_Info_Enabled==1)||(is_Info_Enabled==2)); + return isInfoEnabled(); + } + }; + + /** A fastest possible test if Warn logging is enabled + @return true if enable, false if not. Life time constant. + @see #logger + */ + protected final boolean isWarnEnabled() + { + switch(is_Warn_Enabled) + { + case 1: return false; + case 2: return true; + default: + logger(); + assert( (is_Warn_Enabled==1)||(is_Warn_Enabled==2)); + return isWarnEnabled(); + } + }; + + /** A fastest possible test if Error logging is enabled + @return true if enable, false if not. Life time constant. + @see #logger + */ + protected final boolean isErrorEnabled() + { + switch(is_Error_Enabled) + { + case 1: return false; + case 2: return true; + default: + logger(); + assert( (is_Error_Enabled==1)||(is_Error_Enabled==2)); + return isErrorEnabled(); + } + }; private void customizeHeader() { if (app().settings().getBoolean(Keys.web.useResponsiveLayout, true)) { From dac46a4d5c6951982e5aa3d122402cb1bfbfcc3b Mon Sep 17 00:00:00 2001 From: Tomasz Sztejka Date: Wed, 3 Nov 2021 12:44:00 +0100 Subject: [PATCH 12/21] 1.Armed CommitPage with trace logs which will help pin-point "large commit crashes server" bug. 2.Pin-pointed it to JGitUtils.getFilesInCommit --- src/main/java/com/gitblit/wicket/pages/CommitPage.java | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/main/java/com/gitblit/wicket/pages/CommitPage.java b/src/main/java/com/gitblit/wicket/pages/CommitPage.java index 118c718b2..09455ae29 100644 --- a/src/main/java/com/gitblit/wicket/pages/CommitPage.java +++ b/src/main/java/com/gitblit/wicket/pages/CommitPage.java @@ -62,12 +62,10 @@ public class CommitPage extends RepositoryPage { public CommitPage(PageParameters params) { super(params); - - Repository r = getRepository(); RevCommit c = getCommit(); - logger().trace("Building commits page for:"+r+" commit "+c); + if (isTraceEnabled()) logger().trace("Building commits page for:"+r+" commit "+c); List parents = new ArrayList(); if (c.getParentCount() > 0) { @@ -155,8 +153,9 @@ public void populateItem(final Item item) { add(notesView.setVisible(notes.size() > 0)); // changed paths list + if (isTraceEnabled()) logger().trace("Loading changed paths"); List paths = JGitUtils.getFilesInCommit(r, c); - + if (isTraceEnabled()) logger().trace("Finished loading changed paths"); // add commit diffstat int insertions = 0; int deletions = 0; @@ -335,6 +334,8 @@ public void write(OutputStream output) { } }; add(pathsView); + + if (isTraceEnabled()) logger().trace("Building commits page for:"+r+" commit "+c+" finished."); } @Override From a8f0236eec4021442889de2fa3146fda4f674d46 Mon Sep 17 00:00:00 2001 From: Tomasz Sztejka Date: Wed, 3 Nov 2021 12:51:29 +0100 Subject: [PATCH 13/21] 1.Detected a potential problem with loggers initialization. Server GitBlitServer do initialize static field in instance constructor. This is not a problem. The problem is, that it also initializes background loggers overriding some settings. Most of gitblit.utils classes is using private static final Logger logger = initialization scheme, that is inside a "class constructor". Java strictly says, that classes are initialized before first use. The "use" is any kind of "touch" to class, either directly or in-directly. Since this is very hard to control one may assume, that some static initializers may get called before GitBlitServer reaches logging set-up and construction. Depending on underlying back-end previously fetched loggers may be miss-configured, non-configured or something unpredictable may happen. Changing initialization scheme to be as soon as possible. --- src/main/java/com/gitblit/GitBlitServer.java | 20 +- .../java/com/gitblit/utils/#JGitUtils.java# | 3122 +++++++++++++++++ 2 files changed, 3141 insertions(+), 1 deletion(-) create mode 100644 src/main/java/com/gitblit/utils/#JGitUtils.java# diff --git a/src/main/java/com/gitblit/GitBlitServer.java b/src/main/java/com/gitblit/GitBlitServer.java index 1a72cbf48..830c3c242 100644 --- a/src/main/java/com/gitblit/GitBlitServer.java +++ b/src/main/java/com/gitblit/GitBlitServer.java @@ -83,7 +83,25 @@ */ public class GitBlitServer { - private static Logger logger; + /* Design notes: + + The logger initialization in classes along this application must + have a well behaved policy and either use static initialization: + + static final Logger logger =... + + or dynamic, per instance + + final Logger logger = .... + + Since I did observe that both policies are mixed server MUST + initialize all logging facilities BEFORE any other class is + REFERENCED (by touching any field or etc.). If it is done + differently there is a possibility that static loaders will + pick-up loggers BEFORE server will have a chance to initialize + static field. + */ + private static final Logger logger; public static void main(String... args) { GitBlitServer server = new GitBlitServer(); diff --git a/src/main/java/com/gitblit/utils/#JGitUtils.java# b/src/main/java/com/gitblit/utils/#JGitUtils.java# new file mode 100644 index 000000000..e70b4f996 --- /dev/null +++ b/src/main/java/com/gitblit/utils/#JGitUtils.java# @@ -0,0 +1,3122 @@ +/* + * Copyright 2011 gitblit.com. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.gitblit.utils; + +import java.io.File; +import java.io.IOException; +import java.text.DecimalFormat; +import java.text.MessageFormat; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.Date; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.Map.Entry; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import org.apache.commons.io.filefilter.TrueFileFilter; +import org.eclipse.jgit.api.CloneCommand; +import org.eclipse.jgit.api.FetchCommand; +import org.eclipse.jgit.api.Git; +import org.eclipse.jgit.api.TagCommand; +import org.eclipse.jgit.api.errors.ConcurrentRefUpdateException; +import org.eclipse.jgit.api.errors.GitAPIException; +import org.eclipse.jgit.api.errors.JGitInternalException; +import org.eclipse.jgit.diff.DiffEntry; +import org.eclipse.jgit.diff.DiffEntry.ChangeType; +import org.eclipse.jgit.diff.DiffFormatter; +import org.eclipse.jgit.diff.RawTextComparator; +import org.eclipse.jgit.dircache.DirCache; +import org.eclipse.jgit.dircache.DirCacheEntry; +import org.eclipse.jgit.errors.AmbiguousObjectException; +import org.eclipse.jgit.errors.ConfigInvalidException; +import org.eclipse.jgit.errors.IncorrectObjectTypeException; +import org.eclipse.jgit.errors.LargeObjectException; +import org.eclipse.jgit.errors.MissingObjectException; +import org.eclipse.jgit.errors.RevisionSyntaxException; +import org.eclipse.jgit.errors.StopWalkException; +import org.eclipse.jgit.internal.JGitText; +import org.eclipse.jgit.lib.AnyObjectId; +import org.eclipse.jgit.lib.BlobBasedConfig; +import org.eclipse.jgit.lib.CommitBuilder; +import org.eclipse.jgit.lib.Constants; +import org.eclipse.jgit.lib.FileMode; +import org.eclipse.jgit.lib.ObjectId; +import org.eclipse.jgit.lib.ObjectInserter; +import org.eclipse.jgit.lib.ObjectLoader; +import org.eclipse.jgit.lib.PersonIdent; +import org.eclipse.jgit.lib.Ref; +import org.eclipse.jgit.lib.RefUpdate; +import org.eclipse.jgit.lib.RefUpdate.Result; +import org.eclipse.jgit.lib.Repository; +import org.eclipse.jgit.lib.RepositoryCache.FileKey; +import org.eclipse.jgit.lib.StoredConfig; +import org.eclipse.jgit.lib.TreeFormatter; +import org.eclipse.jgit.merge.MergeStrategy; +import org.eclipse.jgit.merge.RecursiveMerger; +import org.eclipse.jgit.merge.ThreeWayMerger; +import org.eclipse.jgit.revwalk.RevBlob; +import org.eclipse.jgit.revwalk.RevCommit; +import org.eclipse.jgit.revwalk.RevObject; +import org.eclipse.jgit.revwalk.RevSort; +import org.eclipse.jgit.revwalk.RevTree; +import org.eclipse.jgit.revwalk.RevWalk; +import org.eclipse.jgit.revwalk.filter.CommitTimeRevFilter; +import org.eclipse.jgit.revwalk.filter.RevFilter; +import org.eclipse.jgit.storage.file.FileRepositoryBuilder; +import org.eclipse.jgit.transport.CredentialsProvider; +import org.eclipse.jgit.transport.FetchResult; +import org.eclipse.jgit.transport.RefSpec; +import org.eclipse.jgit.treewalk.CanonicalTreeParser; +import org.eclipse.jgit.treewalk.TreeWalk; +import org.eclipse.jgit.treewalk.filter.AndTreeFilter; +import org.eclipse.jgit.treewalk.filter.OrTreeFilter; +import org.eclipse.jgit.treewalk.filter.PathFilter; +import org.eclipse.jgit.treewalk.filter.PathFilterGroup; +import org.eclipse.jgit.treewalk.filter.PathSuffixFilter; +import org.eclipse.jgit.treewalk.filter.TreeFilter; +import org.eclipse.jgit.util.FS; +import org.jetbrains.annotations.NotNull; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.gitblit.Constants.MergeType; +import com.gitblit.GitBlitException; +import com.gitblit.IStoredSettings; +import com.gitblit.Keys; +import com.gitblit.git.PatchsetCommand; +import com.gitblit.models.FilestoreModel; +import com.gitblit.models.GitNote; +import com.gitblit.models.PathModel; +import com.gitblit.models.PathModel.PathChangeModel; +import com.gitblit.models.TicketModel.TicketAction; +import com.gitblit.models.TicketModel.TicketLink; +import com.gitblit.models.RefModel; +import com.gitblit.models.SubmoduleModel; +import com.google.common.base.Strings; + +/** + * Collection of static methods for retrieving information from a repository. + * + * @author James Moger + * + */ +public class JGitUtils { + + static final Logger LOGGER = LoggerFactory.getLogger(JGitUtils.class); + + /** + * Log an error message and exception. + * + * @param t + * @param repository + * if repository is not null it MUST be the {0} parameter in the + * pattern. + * @param pattern + * @param objects + */ + private static void error(Throwable t, Repository repository, String pattern, Object... objects) { + List parameters = new ArrayList(); + if (objects != null && objects.length > 0) { + for (Object o : objects) { + parameters.add(o); + } + } + if (repository != null) { + parameters.add(0, repository.getDirectory().getAbsolutePath()); + } + LOGGER.error(MessageFormat.format(pattern, parameters.toArray()), t); + } + + /** + * Returns the displayable name of the person in the form "Real Name ". If the email address is empty, just "Real Name" is returned. + * + * @param person + * @return "Real Name " or "Real Name" + */ + public static String getDisplayName(PersonIdent person) { + if (StringUtils.isEmpty(person.getEmailAddress())) { + return person.getName(); + } + final StringBuilder r = new StringBuilder(); + r.append(person.getName()); + r.append(" <"); + r.append(person.getEmailAddress()); + r.append('>'); + return r.toString().trim(); + } + + /** + * Encapsulates the result of cloning or pulling from a repository. + */ + public static class CloneResult { + public String name; + public FetchResult fetchResult; + public boolean createdRepository; + } + + /** + * Clone or Fetch a repository. If the local repository does not exist, + * clone is called. If the repository does exist, fetch is called. By + * default the clone/fetch retrieves the remote heads, tags, and notes. + * + * @param repositoriesFolder + * @param name + * @param fromUrl + * @return CloneResult + * @throws Exception + */ + public static CloneResult cloneRepository(File repositoriesFolder, String name, String fromUrl) + throws Exception { + return cloneRepository(repositoriesFolder, name, fromUrl, true, null); + } + + /** + * Clone or Fetch a repository. If the local repository does not exist, + * clone is called. If the repository does exist, fetch is called. By + * default the clone/fetch retrieves the remote heads, tags, and notes. + * + * @param repositoriesFolder + * @param name + * @param fromUrl + * @param bare + * @param credentialsProvider + * @return CloneResult + * @throws Exception + */ + public static CloneResult cloneRepository(File repositoriesFolder, String name, String fromUrl, + boolean bare, CredentialsProvider credentialsProvider) throws Exception { + CloneResult result = new CloneResult(); + if (bare) { + // bare repository, ensure .git suffix + if (!name.toLowerCase().endsWith(Constants.DOT_GIT_EXT)) { + name += Constants.DOT_GIT_EXT; + } + } else { + // normal repository, strip .git suffix + if (name.toLowerCase().endsWith(Constants.DOT_GIT_EXT)) { + name = name.substring(0, name.indexOf(Constants.DOT_GIT_EXT)); + } + } + result.name = name; + + File folder = new File(repositoriesFolder, name); + if (folder.exists()) { + File gitDir = FileKey.resolve(new File(repositoriesFolder, name), FS.DETECTED); + Repository repository = new FileRepositoryBuilder().setGitDir(gitDir).build(); + result.fetchResult = fetchRepository(credentialsProvider, repository); + repository.close(); + } else { + CloneCommand clone = new CloneCommand(); + clone.setBare(bare); + clone.setCloneAllBranches(true); + clone.setURI(fromUrl); + clone.setDirectory(folder); + if (credentialsProvider != null) { + clone.setCredentialsProvider(credentialsProvider); + } + Repository repository = clone.call().getRepository(); + + // Now we have to fetch because CloneCommand doesn't fetch + // refs/notes nor does it allow manual RefSpec. + result.createdRepository = true; + result.fetchResult = fetchRepository(credentialsProvider, repository); + repository.close(); + } + return result; + } + + /** + * Fetch updates from the remote repository. If refSpecs is unspecifed, + * remote heads, tags, and notes are retrieved. + * + * @param credentialsProvider + * @param repository + * @param refSpecs + * @return FetchResult + * @throws Exception + */ + public static FetchResult fetchRepository(CredentialsProvider credentialsProvider, + Repository repository, RefSpec... refSpecs) throws Exception { + Git git = new Git(repository); + FetchCommand fetch = git.fetch(); + List specs = new ArrayList(); + if (refSpecs == null || refSpecs.length == 0) { + specs.add(new RefSpec("+refs/heads/*:refs/remotes/origin/*")); + specs.add(new RefSpec("+refs/tags/*:refs/tags/*")); + specs.add(new RefSpec("+refs/notes/*:refs/notes/*")); + } else { + specs.addAll(Arrays.asList(refSpecs)); + } + if (credentialsProvider != null) { + fetch.setCredentialsProvider(credentialsProvider); + } + fetch.setRefSpecs(specs); + FetchResult fetchRes = fetch.call(); + return fetchRes; + } + + /** + * Creates a bare repository. + * + * @param repositoriesFolder + * @param name + * @return Repository + */ + public static Repository createRepository(File repositoriesFolder, String name) { + return createRepository(repositoriesFolder, name, "FALSE"); + } + + /** + * Creates a bare, shared repository. + * + * @param repositoriesFolder + * @param name + * @param shared + * the setting for the --shared option of "git init". + * @return Repository + */ + public static Repository createRepository(File repositoriesFolder, String name, String shared) { + try { + Repository repo = null; + try { + Git git = Git.init().setDirectory(new File(repositoriesFolder, name)).setBare(true).call(); + repo = git.getRepository(); + } catch (GitAPIException e) { + throw new RuntimeException(e); + } + + GitConfigSharedRepository sharedRepository = new GitConfigSharedRepository(shared); + if (sharedRepository.isShared()) { + StoredConfig config = repo.getConfig(); + config.setString("core", null, "sharedRepository", sharedRepository.getValue()); + config.setBoolean("receive", null, "denyNonFastforwards", true); + config.save(); + + if (! JnaUtils.isWindows()) { + Iterator iter = org.apache.commons.io.FileUtils.iterateFilesAndDirs(repo.getDirectory(), + TrueFileFilter.INSTANCE, TrueFileFilter.INSTANCE); + // Adjust permissions on file/directory + while (iter.hasNext()) { + adjustSharedPerm(iter.next(), sharedRepository); + } + } + } + + return repo; + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + private enum GitConfigSharedRepositoryValue + { + UMASK("0", 0), FALSE("0", 0), OFF("0", 0), NO("0", 0), + GROUP("1", 0660), TRUE("1", 0660), ON("1", 0660), YES("1", 0660), + ALL("2", 0664), WORLD("2", 0664), EVERYBODY("2", 0664), + Oxxx(null, -1); + + private String configValue; + private int permValue; + private GitConfigSharedRepositoryValue(String config, int perm) { configValue = config; permValue = perm; }; + + public String getConfigValue() { return configValue; }; + public int getPerm() { return permValue; }; + + } + + private static class GitConfigSharedRepository + { + private int intValue; + private GitConfigSharedRepositoryValue enumValue; + + GitConfigSharedRepository(String s) { + if ( s == null || s.trim().isEmpty() ) { + enumValue = GitConfigSharedRepositoryValue.GROUP; + } + else { + try { + // Try one of the string values + enumValue = GitConfigSharedRepositoryValue.valueOf(s.trim().toUpperCase()); + } catch (IllegalArgumentException iae) { + try { + // Try if this is an octal number + int i = Integer.parseInt(s, 8); + if ( (i & 0600) != 0600 ) { + String msg = String.format("Problem with core.sharedRepository filemode value (0%03o).\nThe owner of files must always have read and write permissions.", i); + throw new IllegalArgumentException(msg); + } + intValue = i & 0666; + enumValue = GitConfigSharedRepositoryValue.Oxxx; + } catch (NumberFormatException nfe) { + throw new IllegalArgumentException("Bad configuration value for 'shared': '" + s + "'"); + } + } + } + } + + String getValue() { + if ( enumValue == GitConfigSharedRepositoryValue.Oxxx ) { + if (intValue == 0) return "0"; + return String.format("0%o", intValue); + } + return enumValue.getConfigValue(); + } + + int getPerm() { + if ( enumValue == GitConfigSharedRepositoryValue.Oxxx ) return intValue; + return enumValue.getPerm(); + } + + boolean isCustom() { + return enumValue == GitConfigSharedRepositoryValue.Oxxx; + } + + boolean isShared() { + return (enumValue.getPerm() > 0) || enumValue == GitConfigSharedRepositoryValue.Oxxx; + } + } + + + /** + * Adjust file permissions of a file/directory for shared repositories + * + * @param path + * File that should get its permissions changed. + * @param configShared + * Configuration string value for the shared mode. + * @return Upon successful completion, a value of 0 is returned. Otherwise, a value of -1 is returned. + */ + public static int adjustSharedPerm(File path, String configShared) { + return adjustSharedPerm(path, new GitConfigSharedRepository(configShared)); + } + + + /** + * Adjust file permissions of a file/directory for shared repositories + * + * @param path + * File that should get its permissions changed. + * @param configShared + * Configuration setting for the shared mode. + * @return Upon successful completion, a value of 0 is returned. Otherwise, a value of -1 is returned. + */ + public static int adjustSharedPerm(File path, GitConfigSharedRepository configShared) { + if (! configShared.isShared()) return 0; + if (! path.exists()) return -1; + + int perm = configShared.getPerm(); + JnaUtils.Filestat stat = JnaUtils.getFilestat(path); + if (stat == null) return -1; + int mode = stat.mode; + if (mode < 0) return -1; + + // Now, here is the kicker: Under Linux, chmod'ing a sgid file whose guid is different from the process' + // effective guid will reset the sgid flag of the file. Since there is no way to get the sgid flag back in + // that case, we decide to rather not touch is and getting the right permissions will have to be achieved + // in a different way, e.g. by using an appropriate umask for the Gitblit process. + if (System.getProperty("os.name").toLowerCase().startsWith("linux")) { + if ( ((mode & (JnaUtils.S_ISGID | JnaUtils.S_ISUID)) != 0) + && stat.gid != JnaUtils.getegid() ) { + LOGGER.debug("Not adjusting permissions to prevent clearing suid/sgid bits for '" + path + "'" ); + return 0; + } + } + + // If the owner has no write access, delete it from group and other, too. + if ((mode & JnaUtils.S_IWUSR) == 0) perm &= ~0222; + // If the owner has execute access, set it for all blocks that have read access. + if ((mode & JnaUtils.S_IXUSR) == JnaUtils.S_IXUSR) perm |= (perm & 0444) >> 2; + + if (configShared.isCustom()) { + // Use the custom value for access permissions. + mode = (mode & ~0777) | perm; + } + else { + // Just add necessary bits to existing permissions. + mode |= perm; + } + + if (path.isDirectory()) { + mode |= (mode & 0444) >> 2; + mode |= JnaUtils.S_ISGID; + } + + return JnaUtils.setFilemode(path, mode); + } + + + /** + * Returns a list of repository names in the specified folder. + * + * @param repositoriesFolder + * @param onlyBare + * if true, only bare repositories repositories are listed. If + * false all repositories are included. + * @param searchSubfolders + * recurse into subfolders to find grouped repositories + * @param depth + * optional recursion depth, -1 = infinite recursion + * @param exclusions + * list of regex exclusions for matching to folder names + * @return list of repository names + */ + public static List getRepositoryList(File repositoriesFolder, boolean onlyBare, + boolean searchSubfolders, int depth, List exclusions) { + List list = new ArrayList(); + if (repositoriesFolder == null || !repositoriesFolder.exists()) { + return list; + } + List patterns = new ArrayList(); + if (!ArrayUtils.isEmpty(exclusions)) { + for (String regex : exclusions) { + patterns.add(Pattern.compile(regex)); + } + } + list.addAll(getRepositoryList(repositoriesFolder.getAbsolutePath(), repositoriesFolder, + onlyBare, searchSubfolders, depth, patterns)); + StringUtils.sortRepositorynames(list); + list.remove(".git"); // issue-256 + return list; + } + + /** + * Recursive function to find git repositories. + * + * @param basePath + * basePath is stripped from the repository name as repositories + * are relative to this path + * @param searchFolder + * @param onlyBare + * if true only bare repositories will be listed. if false all + * repositories are included. + * @param searchSubfolders + * recurse into subfolders to find grouped repositories + * @param depth + * recursion depth, -1 = infinite recursion + * @param patterns + * list of regex patterns for matching to folder names + * @return + */ + private static List getRepositoryList(String basePath, File searchFolder, + boolean onlyBare, boolean searchSubfolders, int depth, List patterns) { + File baseFile = new File(basePath); + List list = new ArrayList(); + if (depth == 0) { + return list; + } + + int nextDepth = (depth == -1) ? -1 : depth - 1; + for (File file : searchFolder.listFiles()) { + if (file.isDirectory()) { + boolean exclude = false; + for (Pattern pattern : patterns) { + String path = FileUtils.getRelativePath(baseFile, file).replace('\\', '/'); + if (pattern.matcher(path).matches()) { + LOGGER.debug(MessageFormat.format("excluding {0} because of rule {1}", path, pattern.pattern())); + exclude = true; + break; + } + } + if (exclude) { + // skip to next file + continue; + } + + File gitDir = FileKey.resolve(new File(searchFolder, file.getName()), FS.DETECTED); + if (gitDir != null) { + if (onlyBare && gitDir.getName().equals(".git")) { + continue; + } + if (gitDir.equals(file) || gitDir.getParentFile().equals(file)) { + // determine repository name relative to base path + String repository = FileUtils.getRelativePath(baseFile, file); + list.add(repository); + } else if (searchSubfolders && file.canRead()) { + // look for repositories in subfolders + list.addAll(getRepositoryList(basePath, file, onlyBare, searchSubfolders, + nextDepth, patterns)); + } + } else if (searchSubfolders && file.canRead()) { + // look for repositories in subfolders + list.addAll(getRepositoryList(basePath, file, onlyBare, searchSubfolders, + nextDepth, patterns)); + } + } + } + return list; + } + + /** + * Returns the first commit on a branch. If the repository does not exist or + * is empty, null is returned. + * + * @param repository + * @param branch + * if unspecified, HEAD is assumed. + * @return RevCommit + */ + public static RevCommit getFirstCommit(Repository repository, String branch) { + if (!hasCommits(repository)) { + return null; + } + RevCommit commit = null; + try { + // resolve branch + ObjectId branchObject; + if (StringUtils.isEmpty(branch)) { + branchObject = getDefaultBranch(repository); + } else { + branchObject = repository.resolve(branch); + } + + RevWalk walk = new RevWalk(repository); + walk.sort(RevSort.REVERSE); + RevCommit head = walk.parseCommit(branchObject); + walk.markStart(head); + commit = walk.next(); + walk.dispose(); + } catch (Throwable t) { + error(t, repository, "{0} failed to determine first commit"); + } + return commit; + } + + /** + * Returns the date of the first commit on a branch. If the repository does + * not exist, Date(0) is returned. If the repository does exist bit is + * empty, the last modified date of the repository folder is returned. + * + * @param repository + * @param branch + * if unspecified, HEAD is assumed. + * @return Date of the first commit on a branch + */ + public static Date getFirstChange(Repository repository, String branch) { + RevCommit commit = getFirstCommit(repository, branch); + if (commit == null) { + if (repository == null || !repository.getDirectory().exists()) { + return new Date(0); + } + // fresh repository + return new Date(repository.getDirectory().lastModified()); + } + return getCommitDate(commit); + } + + /** + * Determine if a repository has any commits. This is determined by checking + * the for loose and packed objects. + * + * @param repository + * @return true if the repository has commits + */ + public static boolean hasCommits(Repository repository) { + if (repository != null && repository.getDirectory().exists()) { + return (new File(repository.getDirectory(), "objects").list().length > 2) + || (new File(repository.getDirectory(), "objects/pack").list().length > 0); + } + return false; + } + + /** + * Encapsulates the result of cloning or pulling from a repository. + */ + public static class LastChange { + public Date when; + public String who; + + LastChange() { + when = new Date(0); + } + + LastChange(long lastModified) { + this.when = new Date(lastModified); + } + } + + /** + * Returns the date and author of the most recent commit on a branch. If the + * repository does not exist Date(0) is returned. If it does exist but is + * empty, the last modified date of the repository folder is returned. + * + * @param repository + * @return a LastChange object + */ + public static LastChange getLastChange(Repository repository) { + if (!hasCommits(repository)) { + // null repository + if (repository == null) { + return new LastChange(); + } + // fresh repository + return new LastChange(repository.getDirectory().lastModified()); + } + + List branchModels = getLocalBranches(repository, true, -1); + if (branchModels.size() > 0) { + // find most recent branch update + LastChange lastChange = new LastChange(); + for (RefModel branchModel : branchModels) { + if (branchModel.getDate().after(lastChange.when)) { + lastChange.when = branchModel.getDate(); + lastChange.who = branchModel.getAuthorIdent().getName(); + } + } + return lastChange; + } + + // default to the repository folder modification date + return new LastChange(repository.getDirectory().lastModified()); + } + + /** + * Retrieves a Java Date from a Git commit. + * + * @param commit + * @return date of the commit or Date(0) if the commit is null + */ + public static Date getCommitDate(RevCommit commit) { + if (commit == null) { + return new Date(0); + } + return new Date(commit.getCommitTime() * 1000L); + } + + /** + * Retrieves a Java Date from a Git commit. + * + * @param commit + * @return date of the commit or Date(0) if the commit is null + */ + public static Date getAuthorDate(RevCommit commit) { + if (commit == null) { + return new Date(0); + } + if (commit.getAuthorIdent() != null) { + return commit.getAuthorIdent().getWhen(); + } + return getCommitDate(commit); + } + + /** + * Returns the specified commit from the repository. If the repository does + * not exist or is empty, null is returned. + * + * @param repository + * @param objectId + * if unspecified, HEAD is assumed. + * @return RevCommit + */ + public static RevCommit getCommit(Repository repository, String objectId) { + if (!hasCommits(repository)) { + return null; + } + RevCommit commit = null; + RevWalk walk = null; + try { + // resolve object id + ObjectId branchObject; + if (StringUtils.isEmpty(objectId) || "HEAD".equalsIgnoreCase(objectId)) { + branchObject = getDefaultBranch(repository); + } else { + branchObject = repository.resolve(objectId); + } + if (branchObject == null) { + return null; + } + walk = new RevWalk(repository); + RevCommit rev = walk.parseCommit(branchObject); + commit = rev; + } catch (Throwable t) { + error(t, repository, "{0} failed to get commit {1}", objectId); + } finally { + if (walk != null) { + walk.dispose(); + } + } + return commit; + } + + /** + * Retrieves the raw byte content of a file in the specified tree. + * + * @param repository + * @param tree + * if null, the RevTree from HEAD is assumed. + * @param path + * @return content as a byte [] + */ + public static byte[] getByteContent(Repository repository, RevTree tree, final String path, boolean throwError) { + RevWalk rw = new RevWalk(repository); + TreeWalk tw = new TreeWalk(repository); + tw.setFilter(PathFilterGroup.createFromStrings(Collections.singleton(path))); + byte[] content = null; + try { + if (tree == null) { + ObjectId object = getDefaultBranch(repository); + if (object == null) + return null; + RevCommit commit = rw.parseCommit(object); + tree = commit.getTree(); + } + tw.reset(tree); + while (tw.next()) { + if (tw.isSubtree() && !path.equals(tw.getPathString())) { + tw.enterSubtree(); + continue; + } + ObjectId entid = tw.getObjectId(0); + FileMode entmode = tw.getFileMode(0); + if (entmode != FileMode.GITLINK) { + ObjectLoader ldr = repository.open(entid, Constants.OBJ_BLOB); + content = ldr.getCachedBytes(); + } + } + } catch (Throwable t) { + if (throwError) { + error(t, repository, "{0} can't find {1} in tree {2}", path, tree.name()); + } + } finally { + rw.dispose(); + tw.close(); + } + return content; + } + + /** + * Returns the UTF-8 string content of a file in the specified tree. + * + * @param repository + * @param tree + * if null, the RevTree from HEAD is assumed. + * @param blobPath + * @param charsets optional + * @return UTF-8 string content + */ + public static String getStringContent(Repository repository, RevTree tree, String blobPath, String... charsets) { + byte[] content = getByteContent(repository, tree, blobPath, true); + if (content == null) { + return null; + } + return StringUtils.decodeString(content, charsets); + } + + /** + * Gets the raw byte content of the specified blob object. + * + * @param repository + * @param objectId + * @return byte [] blob content + */ + public static byte[] getByteContent(Repository repository, String objectId) { + RevWalk rw = new RevWalk(repository); + byte[] content = null; + try { + RevBlob blob = rw.lookupBlob(ObjectId.fromString(objectId)); + ObjectLoader ldr = repository.open(blob.getId(), Constants.OBJ_BLOB); + content = ldr.getCachedBytes(); + } catch (Throwable t) { + error(t, repository, "{0} can't find blob {1}", objectId); + } finally { + rw.dispose(); + } + return content; + } + + /** + * Gets the UTF-8 string content of the blob specified by objectId. + * + * @param repository + * @param objectId + * @param charsets optional + * @return UTF-8 string content + */ + public static String getStringContent(Repository repository, String objectId, String... charsets) { + byte[] content = getByteContent(repository, objectId); + if (content == null) { + return null; + } + return StringUtils.decodeString(content, charsets); + } + + /** + * Returns the list of files in the specified folder at the specified + * commit. If the repository does not exist or is empty, an empty list is + * returned. + * + * @param repository + * @param path + * if unspecified, root folder is assumed. + * @param commit + * if null, HEAD is assumed. + * @return list of files in specified path + */ + public static List getFilesInPath(Repository repository, String path, + RevCommit commit) { + List list = new ArrayList(); + if (!hasCommits(repository)) { + return list; + } + if (commit == null) { + commit = getCommit(repository, null); + } + final TreeWalk tw = new TreeWalk(repository); + try { + tw.addTree(commit.getTree()); + if (!StringUtils.isEmpty(path)) { + PathFilter f = PathFilter.create(path); + tw.setFilter(f); + tw.setRecursive(false); + boolean foundFolder = false; + while (tw.next()) { + if (!foundFolder && tw.isSubtree()) { + tw.enterSubtree(); + } + if (tw.getPathString().equals(path)) { + foundFolder = true; + continue; + } + if (foundFolder) { + list.add(getPathModel(tw, path, commit)); + } + } + } else { + tw.setRecursive(false); + while (tw.next()) { + list.add(getPathModel(tw, null, commit)); + } + } + } catch (IOException e) { + error(e, repository, "{0} failed to get files for commit {1}", commit.getName()); + } finally { + tw.close(); + } + Collections.sort(list); + return list; + } + + /** + * Returns the list of files in the specified folder at the specified + * commit. If the repository does not exist or is empty, an empty list is + * returned. + * + * This is modified version that implements path compression feature. + * + * @param repository + * @param path + * if unspecified, root folder is assumed. + * @param commit + * if null, HEAD is assumed. + * @return list of files in specified path + */ + public static List getFilesInPath2(Repository repository, String path, RevCommit commit) { + + List list = new ArrayList(); + if (!hasCommits(repository)) { + return list; + } + if (commit == null) { + commit = getCommit(repository, null); + } + final TreeWalk tw = new TreeWalk(repository); + try { + + tw.addTree(commit.getTree()); + final boolean isPathEmpty = Strings.isNullOrEmpty(path); + + if (!isPathEmpty) { + PathFilter f = PathFilter.create(path); + tw.setFilter(f); + } + + tw.setRecursive(true); + List paths = new ArrayList<>(); + + while (tw.next()) { + String pathString = tw.getPathString(); + String child = isPathEmpty ? pathString : pathString.replaceFirst(Pattern.quote(String.format("%s/", path)), ""); + paths.add(child); + } + + for(String p: PathUtils.compressPaths(paths)) { + String pathString = isPathEmpty ? p : String.format("%s/%s", path, p); + list.add(getPathModel(repository, pathString, path, commit)); + } + + } catch (IOException e) { + error(e, repository, "{0} failed to get files for commit {1}", commit.getName()); + } finally { + tw.close(); + } + Collections.sort(list); + return list; + } + + /** + * Returns the list of files changed in a specified commit. If the + * repository does not exist or is empty, an empty list is returned. + * + * @param repository + * @param commit + * if null, HEAD is assumed. + * @return list of files changed in a commit + */ + public static List getFilesInCommit(Repository repository, RevCommit commit) { + return getFilesInCommit(repository, commit, true); + } + + /** + * Returns the list of files changed in a specified commit. If the + * repository does not exist or is empty, an empty list is returned. + * + * @param repository + * @param commit + * if null, HEAD is assumed. + * @param calculateDiffStat + * if true, each PathChangeModel will have insertions/deletions + * @return list of files changed in a commit + */ + public static List getFilesInCommit(Repository repository, RevCommit commit, boolean calculateDiffStat) { + List list = new ArrayList(); + if (!hasCommits(repository)) { + return list; + } + RevWalk rw = new RevWalk(repository); + try { + if (commit == null) { + ObjectId object = getDefaultBranch(repository); + commit = rw.parseCommit(object); + } + + if (commit.getParentCount() == 0) { + TreeWalk tw = new TreeWalk(repository); + tw.reset(); + tw.setRecursive(true); + tw.addTree(commit.getTree()); + while (tw.next()) { + long size = 0; + FilestoreModel filestoreItem = null; + ObjectId objectId = tw.getObjectId(0); + + try { + if (!tw.isSubtree() && (tw.getFileMode(0) != FileMode.GITLINK)) { + + size = tw.getObjectReader().getObjectSize(objectId, Constants.OBJ_BLOB); + + if (isPossibleFilestoreItem(size)) { + filestoreItem = getFilestoreItem(tw.getObjectReader().open(objectId)); + } + } + } catch (Throwable t) { + error(t, null, "failed to retrieve blob size for " + tw.getPathString()); + } + + list.add(new PathChangeModel(tw.getPathString(), tw.getPathString(),filestoreItem, size, tw + .getRawMode(0), objectId.getName(), commit.getId().getName(), + ChangeType.ADD)); + } + tw.close(); + } else { + RevCommit parent = rw.parseCommit(commit.getParent(0).getId()); + DiffStatFormatter df = new DiffStatFormatter(commit.getName(), repository); + df.setRepository(repository); + df.setDiffComparator(RawTextComparator.DEFAULT); + df.setDetectRenames(true); + List diffs = df.scan(parent.getTree(), commit.getTree()); + for (DiffEntry diff : diffs) { + // create the path change model + PathChangeModel pcm = PathChangeModel.from(diff, commit.getName(), repository); + + if (calculateDiffStat) { + // update file diffstats + df.format(diff); + PathChangeModel pathStat = df.getDiffStat().getPath(pcm.path); + if (pathStat != null) { + pcm.insertions = pathStat.insertions; + pcm.deletions = pathStat.deletions; + } + } + list.add(pcm); + } + } + } catch (Throwable t) { + error(t, repository, "{0} failed to determine files in commit!"); + } finally { + rw.dispose(); + } + return list; + } + + /** + * Returns the list of files changed in a specified commit. If the + * repository does not exist or is empty, an empty list is returned. + * + * @param repository + * @param startCommit + * earliest commit + * @param endCommit + * most recent commit. if null, HEAD is assumed. + * @return list of files changed in a commit range + */ + public static List getFilesInRange(Repository repository, String startCommit, String endCommit) { + List list = new ArrayList(); + if (!hasCommits(repository)) { + return list; + } + try { + ObjectId startRange = repository.resolve(startCommit); + ObjectId endRange = repository.resolve(endCommit); + RevWalk rw = new RevWalk(repository); + RevCommit start = rw.parseCommit(startRange); + RevCommit end = rw.parseCommit(endRange); + list.addAll(getFilesInRange(repository, start, end)); + rw.close(); + } catch (Throwable t) { + error(t, repository, "{0} failed to determine files in range {1}..{2}!", startCommit, endCommit); + } + return list; + } + + /** + * Returns the list of files changed in a specified commit. If the + * repository does not exist or is empty, an empty list is returned. + * + * @param repository + * @param startCommit + * earliest commit + * @param endCommit + * most recent commit. if null, HEAD is assumed. + * @return list of files changed in a commit range + */ + public static List getFilesInRange(Repository repository, RevCommit startCommit, RevCommit endCommit) { + List list = new ArrayList(); + if (!hasCommits(repository)) { + return list; + } + try { + DiffFormatter df = new DiffFormatter(null); + df.setRepository(repository); + df.setDiffComparator(RawTextComparator.DEFAULT); + df.setDetectRenames(true); + + List diffEntries = df.scan(startCommit.getTree(), endCommit.getTree()); + for (DiffEntry diff : diffEntries) { + PathChangeModel pcm = PathChangeModel.from(diff, endCommit.getName(), repository); + list.add(pcm); + } + Collections.sort(list); + } catch (Throwable t) { + error(t, repository, "{0} failed to determine files in range {1}..{2}!", startCommit, endCommit); + } + return list; + } + /** + * Returns the list of files in the repository on the default branch that + * match one of the specified extensions. This is a CASE-SENSITIVE search. + * If the repository does not exist or is empty, an empty list is returned. + * + * @param repository + * @param extensions + * @return list of files in repository with a matching extension + */ + public static List getDocuments(Repository repository, List extensions) { + return getDocuments(repository, extensions, null); + } + + /** + * Returns the list of files in the repository in the specified commit that + * match one of the specified extensions. This is a CASE-SENSITIVE search. + * If the repository does not exist or is empty, an empty list is returned. + * + * @param repository + * @param extensions + * @param objectId + * @return list of files in repository with a matching extension + */ + public static List getDocuments(Repository repository, List extensions, + String objectId) { + List list = new ArrayList(); + if (!hasCommits(repository)) { + return list; + } + RevCommit commit = getCommit(repository, objectId); + final TreeWalk tw = new TreeWalk(repository); + try { + tw.addTree(commit.getTree()); + if (extensions != null && extensions.size() > 0) { + List suffixFilters = new ArrayList(); + for (String extension : extensions) { + if (extension.charAt(0) == '.') { + suffixFilters.add(PathSuffixFilter.create(extension)); + } else { + // escape the . since this is a regexp filter + suffixFilters.add(PathSuffixFilter.create("." + extension)); + } + } + TreeFilter filter; + if (suffixFilters.size() == 1) { + filter = suffixFilters.get(0); + } else { + filter = OrTreeFilter.create(suffixFilters); + } + tw.setFilter(filter); + tw.setRecursive(true); + } + while (tw.next()) { + list.add(getPathModel(tw, null, commit)); + } + } catch (IOException e) { + error(e, repository, "{0} failed to get documents for commit {1}", commit.getName()); + } finally { + tw.close(); + } + Collections.sort(list); + return list; + } + + /** + * Returns a path model of the current file in the treewalk. + * + * @param tw + * @param basePath + * @param commit + * @return a path model of the current file in the treewalk + */ + private static PathModel getPathModel(TreeWalk tw, String basePath, RevCommit commit) { + String name; + long size = 0; + + if (StringUtils.isEmpty(basePath)) { + name = tw.getPathString(); + } else { + name = tw.getPathString().substring(basePath.length() + 1); + } + ObjectId objectId = tw.getObjectId(0); + FilestoreModel filestoreItem = null; + + try { + if (!tw.isSubtree() && (tw.getFileMode(0) != FileMode.GITLINK)) { + + size = tw.getObjectReader().getObjectSize(objectId, Constants.OBJ_BLOB); + + if (isPossibleFilestoreItem(size)) { + filestoreItem = getFilestoreItem(tw.getObjectReader().open(objectId)); + } + } + } catch (Throwable t) { + error(t, null, "failed to retrieve blob size for " + tw.getPathString()); + } + return new PathModel(name, tw.getPathString(), filestoreItem, size, tw.getFileMode(0).getBits(), + objectId.getName(), commit.getName()); + } + + public static boolean isPossibleFilestoreItem(long size) { + return ( (size >= com.gitblit.Constants.LEN_FILESTORE_META_MIN) + && (size <= com.gitblit.Constants.LEN_FILESTORE_META_MAX)); + } + + /** + * + * @return Representative FilestoreModel if valid, otherwise null + */ + public static FilestoreModel getFilestoreItem(ObjectLoader obj){ + try { + final byte[] blob = obj.getCachedBytes(com.gitblit.Constants.LEN_FILESTORE_META_MAX); + final String meta = new String(blob, "UTF-8"); + + return FilestoreModel.fromMetaString(meta); + + } catch (LargeObjectException e) { + //Intentionally failing silent + } catch (Exception e) { + error(e, null, "failed to retrieve filestoreItem " + obj.toString()); + } + + return null; + } + + /** + * Returns a path model by path string + * + * @param repo + * @param path + * @param filter + * @param commit + * @return a path model of the specified object + */ + private static PathModel getPathModel(Repository repo, String path, String filter, RevCommit commit) + throws IOException { + + long size = 0; + FilestoreModel filestoreItem = null; + TreeWalk tw = TreeWalk.forPath(repo, path, commit.getTree()); + String pathString = path; + + if (!tw.isSubtree() && (tw.getFileMode(0) != FileMode.GITLINK)) { + + pathString = PathUtils.getLastPathComponent(pathString); + + size = tw.getObjectReader().getObjectSize(tw.getObjectId(0), Constants.OBJ_BLOB); + + if (isPossibleFilestoreItem(size)) { + filestoreItem = getFilestoreItem(tw.getObjectReader().open(tw.getObjectId(0))); + } + } else if (tw.isSubtree()) { + + // do not display dirs that are behind in the path + if (!Strings.isNullOrEmpty(filter)) { + pathString = path.replaceFirst(filter + "/", ""); + } + + // remove the last slash from path in displayed link + if (pathString != null && pathString.charAt(pathString.length()-1) == '/') { + pathString = pathString.substring(0, pathString.length()-1); + } + } + + return new PathModel(pathString, tw.getPathString(), filestoreItem, size, tw.getFileMode(0).getBits(), + tw.getObjectId(0).getName(), commit.getName()); + + } + + + /** + * Returns a permissions representation of the mode bits. + * + * @param mode + * @return string representation of the mode bits + */ + public static String getPermissionsFromMode(int mode) { + if (FileMode.TREE.equals(mode)) { + return "drwxr-xr-x"; + } else if (FileMode.REGULAR_FILE.equals(mode)) { + return "-rw-r--r--"; + } else if (FileMode.EXECUTABLE_FILE.equals(mode)) { + return "-rwxr-xr-x"; + } else if (FileMode.SYMLINK.equals(mode)) { + return "symlink"; + } else if (FileMode.GITLINK.equals(mode)) { + return "submodule"; + } + return "missing"; + } + + /** + * Returns a list of commits since the minimum date starting from the + * specified object id. + * + * @param repository + * @param objectId + * if unspecified, HEAD is assumed. + * @param minimumDate + * @return list of commits + */ + public static List getRevLog(Repository repository, String objectId, Date minimumDate) { + List list = new ArrayList(); + if (!hasCommits(repository)) { + return list; + } + try { + // resolve branch + ObjectId branchObject; + if (StringUtils.isEmpty(objectId)) { + branchObject = getDefaultBranch(repository); + } else { + branchObject = repository.resolve(objectId); + } + + RevWalk rw = new RevWalk(repository); + rw.markStart(rw.parseCommit(branchObject)); + rw.setRevFilter(CommitTimeRevFilter.after(minimumDate)); + Iterable revlog = rw; + for (RevCommit rev : revlog) { + list.add(rev); + } + rw.dispose(); + } catch (Throwable t) { + error(t, repository, "{0} failed to get {1} revlog for minimum date {2}", objectId, + minimumDate); + } + return list; + } + + /** + * Returns a list of commits starting from HEAD and working backwards. + * + * @param repository + * @param maxCount + * if < 0, all commits for the repository are returned. + * @return list of commits + */ + public static List getRevLog(Repository repository, int maxCount) { + return getRevLog(repository, null, 0, maxCount); + } + + /** + * Returns a list of commits starting from the specified objectId using an + * offset and maxCount for paging. This is similar to LIMIT n OFFSET p in + * SQL. If the repository does not exist or is empty, an empty list is + * returned. + * + * @param repository + * @param objectId + * if unspecified, HEAD is assumed. + * @param offset + * @param maxCount + * if < 0, all commits are returned. + * @return a paged list of commits + */ + public static List getRevLog(Repository repository, String objectId, int offset, + int maxCount) { + return getRevLog(repository, objectId, null, offset, maxCount); + } + + /** + * Returns a list of commits for the repository or a path within the + * repository. Caller may specify ending revision with objectId. Caller may + * specify offset and maxCount to achieve pagination of results. If the + * repository does not exist or is empty, an empty list is returned. + * + * @param repository + * @param objectId + * if unspecified, HEAD is assumed. + * @param path + * if unspecified, commits for repository are returned. If + * specified, commits for the path are returned. + * @param offset + * @param maxCount + * if < 0, all commits are returned. + * @return a paged list of commits + */ + public static List getRevLog(Repository repository, String objectId, String path, + int offset, int maxCount) { + List list = new ArrayList(); + if (maxCount == 0) { + return list; + } + if (!hasCommits(repository)) { + return list; + } + try { + // resolve branch + ObjectId startRange = null; + ObjectId endRange; + if (StringUtils.isEmpty(objectId)) { + endRange = getDefaultBranch(repository); + } else { + if( objectId.contains("..") ) { + // range expression + String[] parts = objectId.split("\\.\\."); + startRange = repository.resolve(parts[0]); + endRange = repository.resolve(parts[1]); + } else { + // objectid + endRange= repository.resolve(objectId); + } + } + if (endRange == null) { + return list; + } + + RevWalk rw = new RevWalk(repository); + rw.markStart(rw.parseCommit(endRange)); + if (startRange != null) { + rw.markUninteresting(rw.parseCommit(startRange)); + } + if (!StringUtils.isEmpty(path)) { + TreeFilter filter = AndTreeFilter.create( + PathFilterGroup.createFromStrings(Collections.singleton(path)), + TreeFilter.ANY_DIFF); + rw.setTreeFilter(filter); + } + Iterable revlog = rw; + if (offset > 0) { + int count = 0; + for (RevCommit rev : revlog) { + count++; + if (count > offset) { + list.add(rev); + if (maxCount > 0 && list.size() == maxCount) { + break; + } + } + } + } else { + for (RevCommit rev : revlog) { + list.add(rev); + if (maxCount > 0 && list.size() == maxCount) { + break; + } + } + } + rw.dispose(); + } catch (Throwable t) { + error(t, repository, "{0} failed to get {1} revlog for path {2}", objectId, path); + } + return list; + } + + /** + * Returns a list of commits for the repository within the range specified + * by startRangeId and endRangeId. If the repository does not exist or is + * empty, an empty list is returned. + * + * @param repository + * @param startRangeId + * the first commit (not included in results) + * @param endRangeId + * the end commit (included in results) + * @return a list of commits + */ + public static List getRevLog(Repository repository, String startRangeId, + String endRangeId) { + List list = new ArrayList(); + if (!hasCommits(repository)) { + return list; + } + try { + ObjectId endRange = repository.resolve(endRangeId); + ObjectId startRange = repository.resolve(startRangeId); + + RevWalk rw = new RevWalk(repository); + rw.markStart(rw.parseCommit(endRange)); + if (startRange.equals(ObjectId.zeroId())) { + // maybe this is a tag or an orphan branch + list.add(rw.parseCommit(endRange)); + rw.dispose(); + return list; + } else { + rw.markUninteresting(rw.parseCommit(startRange)); + } + + Iterable revlog = rw; + for (RevCommit rev : revlog) { + list.add(rev); + } + rw.dispose(); + } catch (Throwable t) { + error(t, repository, "{0} failed to get revlog for {1}..{2}", startRangeId, endRangeId); + } + return list; + } + + /** + * Search the commit history for a case-insensitive match to the value. + * Search results require a specified SearchType of AUTHOR, COMMITTER, or + * COMMIT. Results may be paginated using offset and maxCount. If the + * repository does not exist or is empty, an empty list is returned. + * + * @param repository + * @param objectId + * if unspecified, HEAD is assumed. + * @param value + * @param type + * AUTHOR, COMMITTER, COMMIT + * @param offset + * @param maxCount + * if < 0, all matches are returned + * @return matching list of commits + */ + public static List searchRevlogs(Repository repository, String objectId, + String value, final com.gitblit.Constants.SearchType type, int offset, int maxCount) { + List list = new ArrayList(); + if (StringUtils.isEmpty(value)) { + return list; + } + if (maxCount == 0) { + return list; + } + if (!hasCommits(repository)) { + return list; + } + final String lcValue = value.toLowerCase(); + try { + // resolve branch + ObjectId branchObject; + if (StringUtils.isEmpty(objectId)) { + branchObject = getDefaultBranch(repository); + } else { + branchObject = repository.resolve(objectId); + } + + RevWalk rw = new RevWalk(repository); + rw.setRevFilter(new RevFilter() { + + @Override + public RevFilter clone() { + // FindBugs complains about this method name. + // This is part of JGit design and unrelated to Cloneable. + return this; + } + + @Override + public boolean include(RevWalk walker, RevCommit commit) throws StopWalkException, + MissingObjectException, IncorrectObjectTypeException, IOException { + boolean include = false; + switch (type) { + case AUTHOR: + include = (commit.getAuthorIdent().getName().toLowerCase().indexOf(lcValue) > -1) + || (commit.getAuthorIdent().getEmailAddress().toLowerCase() + .indexOf(lcValue) > -1); + break; + case COMMITTER: + include = (commit.getCommitterIdent().getName().toLowerCase() + .indexOf(lcValue) > -1) + || (commit.getCommitterIdent().getEmailAddress().toLowerCase() + .indexOf(lcValue) > -1); + break; + case COMMIT: + include = commit.getFullMessage().toLowerCase().indexOf(lcValue) > -1; + break; + } + return include; + } + + }); + rw.markStart(rw.parseCommit(branchObject)); + Iterable revlog = rw; + if (offset > 0) { + int count = 0; + for (RevCommit rev : revlog) { + count++; + if (count > offset) { + list.add(rev); + if (maxCount > 0 && list.size() == maxCount) { + break; + } + } + } + } else { + for (RevCommit rev : revlog) { + list.add(rev); + if (maxCount > 0 && list.size() == maxCount) { + break; + } + } + } + rw.dispose(); + } catch (Throwable t) { + error(t, repository, "{0} failed to {1} search revlogs for {2}", type.name(), value); + } + return list; + } + + /** + * Returns the default branch to use for a repository. Normally returns + * whatever branch HEAD points to, but if HEAD points to nothing it returns + * the most recently updated branch. + * + * @param repository + * @return the objectid of a branch + * @throws Exception + */ + public static ObjectId getDefaultBranch(Repository repository) throws Exception { + ObjectId object = repository.resolve(Constants.HEAD); + if (object == null) { + // no HEAD + // perhaps non-standard repository, try local branches + List branchModels = getLocalBranches(repository, true, -1); + if (branchModels.size() > 0) { + // use most recently updated branch + RefModel branch = null; + Date lastDate = new Date(0); + for (RefModel branchModel : branchModels) { + if (branchModel.getDate().after(lastDate)) { + branch = branchModel; + lastDate = branch.getDate(); + } + } + object = branch.getReferencedObjectId(); + } + } + return object; + } + + /** + * Returns the target of the symbolic HEAD reference for a repository. + * Normally returns a branch reference name, but when HEAD is detached, + * the commit is matched against the known tags. The most recent matching + * tag ref name will be returned if it references the HEAD commit. If + * no match is found, the SHA1 is returned. + * + * @param repository + * @return the ref name or the SHA1 for a detached HEAD + */ + public static String getHEADRef(Repository repository) { + String target = null; + try { + target = repository.getFullBranch(); + } catch (Throwable t) { + error(t, repository, "{0} failed to get symbolic HEAD target"); + } + return target; + } + + /** + * Sets the symbolic ref HEAD to the specified target ref. The + * HEAD will be detached if the target ref is not a branch. + * + * @param repository + * @param targetRef + * @return true if successful + */ + public static boolean setHEADtoRef(Repository repository, String targetRef) { + try { + // detach HEAD if target ref is not a branch + boolean detach = !targetRef.startsWith(Constants.R_HEADS); + RefUpdate.Result result; + RefUpdate head = repository.updateRef(Constants.HEAD, detach); + if (detach) { // Tag + RevCommit commit = getCommit(repository, targetRef); + head.setNewObjectId(commit.getId()); + result = head.forceUpdate(); + } else { + result = head.link(targetRef); + } + switch (result) { + case NEW: + case FORCED: + case NO_CHANGE: + case FAST_FORWARD: + return true; + default: + LOGGER.error(MessageFormat.format("{0} HEAD update to {1} returned result {2}", + repository.getDirectory().getAbsolutePath(), targetRef, result)); + } + } catch (Throwable t) { + error(t, repository, "{0} failed to set HEAD to {1}", targetRef); + } + return false; + } + + /** + * Sets the local branch ref to point to the specified commit id. + * + * @param repository + * @param branch + * @param commitId + * @return true if successful + */ + public static boolean setBranchRef(Repository repository, String branch, String commitId) { + String branchName = branch; + if (!branchName.startsWith(Constants.R_REFS)) { + branchName = Constants.R_HEADS + branch; + } + + try { + RefUpdate refUpdate = repository.updateRef(branchName, false); + refUpdate.setNewObjectId(ObjectId.fromString(commitId)); + RefUpdate.Result result = refUpdate.forceUpdate(); + + switch (result) { + case NEW: + case FORCED: + case NO_CHANGE: + case FAST_FORWARD: + return true; + default: + LOGGER.error(MessageFormat.format("{0} {1} update to {2} returned result {3}", + repository.getDirectory().getAbsolutePath(), branchName, commitId, result)); + } + } catch (Throwable t) { + error(t, repository, "{0} failed to set {1} to {2}", branchName, commitId); + } + return false; + } + + /** + * Deletes the specified branch ref. + * + * @param repository + * @param branch + * @return true if successful + */ + public static boolean deleteBranchRef(Repository repository, String branch) { + + try { + RefUpdate refUpdate = repository.updateRef(branch, false); + refUpdate.setForceUpdate(true); + RefUpdate.Result result = refUpdate.delete(); + switch (result) { + case NEW: + case FORCED: + case NO_CHANGE: + case FAST_FORWARD: + return true; + default: + LOGGER.error(MessageFormat.format("{0} failed to delete to {1} returned result {2}", + repository.getDirectory().getAbsolutePath(), branch, result)); + } + } catch (Throwable t) { + error(t, repository, "{0} failed to delete {1}", branch); + } + return false; + } + + /** + * Get the full branch and tag ref names for any potential HEAD targets. + * + * @param repository + * @return a list of ref names + */ + public static List getAvailableHeadTargets(Repository repository) { + List targets = new ArrayList(); + for (RefModel branchModel : JGitUtils.getLocalBranches(repository, true, -1)) { + targets.add(branchModel.getName()); + } + + for (RefModel tagModel : JGitUtils.getTags(repository, true, -1)) { + targets.add(tagModel.getName()); + } + return targets; + } + + /** + * Returns all refs grouped by their associated object id. + * + * @param repository + * @return all refs grouped by their referenced object id + */ + public static Map> getAllRefs(Repository repository) { + return getAllRefs(repository, true); + } + + /** + * Returns all refs grouped by their associated object id. + * + * @param repository + * @param includeRemoteRefs + * @return all refs grouped by their referenced object id + */ + public static Map> getAllRefs(Repository repository, boolean includeRemoteRefs) { + List list = getRefs(repository, org.eclipse.jgit.lib.RefDatabase.ALL, true, -1); + Map> refs = new HashMap>(); + for (RefModel ref : list) { + if (!includeRemoteRefs && ref.getName().startsWith(Constants.R_REMOTES)) { + continue; + } + ObjectId objectid = ref.getReferencedObjectId(); + if (!refs.containsKey(objectid)) { + refs.put(objectid, new ArrayList()); + } + refs.get(objectid).add(ref); + } + return refs; + } + + /** + * Returns the list of tags in the repository. If repository does not exist + * or is empty, an empty list is returned. + * + * @param repository + * @param fullName + * if true, /refs/tags/yadayadayada is returned. If false, + * yadayadayada is returned. + * @param maxCount + * if < 0, all tags are returned + * @return list of tags + */ + public static List getTags(Repository repository, boolean fullName, int maxCount) { + return getRefs(repository, Constants.R_TAGS, fullName, maxCount); + } + + /** + * Returns the list of tags in the repository. If repository does not exist + * or is empty, an empty list is returned. + * + * @param repository + * @param fullName + * if true, /refs/tags/yadayadayada is returned. If false, + * yadayadayada is returned. + * @param maxCount + * if < 0, all tags are returned + * @param offset + * if maxCount provided sets the starting point of the records to return + * @return list of tags + */ + public static List getTags(Repository repository, boolean fullName, int maxCount, int offset) { + return getRefs(repository, Constants.R_TAGS, fullName, maxCount, offset); + } + + /** + * Returns the list of local branches in the repository. If repository does + * not exist or is empty, an empty list is returned. + * + * @param repository + * @param fullName + * if true, /refs/heads/yadayadayada is returned. If false, + * yadayadayada is returned. + * @param maxCount + * if < 0, all local branches are returned + * @return list of local branches + */ + public static List getLocalBranches(Repository repository, boolean fullName, + int maxCount) { + return getRefs(repository, Constants.R_HEADS, fullName, maxCount); + } + + /** + * Returns the list of remote branches in the repository. If repository does + * not exist or is empty, an empty list is returned. + * + * @param repository + * @param fullName + * if true, /refs/remotes/yadayadayada is returned. If false, + * yadayadayada is returned. + * @param maxCount + * if < 0, all remote branches are returned + * @return list of remote branches + */ + public static List getRemoteBranches(Repository repository, boolean fullName, + int maxCount) { + return getRefs(repository, Constants.R_REMOTES, fullName, maxCount); + } + + /** + * Returns the list of note branches. If repository does not exist or is + * empty, an empty list is returned. + * + * @param repository + * @param fullName + * if true, /refs/notes/yadayadayada is returned. If false, + * yadayadayada is returned. + * @param maxCount + * if < 0, all note branches are returned + * @return list of note branches + */ + public static List getNoteBranches(Repository repository, boolean fullName, + int maxCount) { + return getRefs(repository, Constants.R_NOTES, fullName, maxCount); + } + + /** + * Returns the list of refs in the specified base ref. If repository does + * not exist or is empty, an empty list is returned. + * + * @param repository + * @param fullName + * if true, /refs/yadayadayada is returned. If false, + * yadayadayada is returned. + * @return list of refs + */ + public static List getRefs(Repository repository, String baseRef) { + return getRefs(repository, baseRef, true, -1); + } + + /** + * Returns a list of references in the repository matching "refs". If the + * repository is null or empty, an empty list is returned. + * + * @param repository + * @param refs + * if unspecified, all refs are returned + * @param fullName + * if true, /refs/something/yadayadayada is returned. If false, + * yadayadayada is returned. + * @param maxCount + * if < 0, all references are returned + * @return list of references + */ + private static List getRefs(Repository repository, String refs, boolean fullName, + int maxCount) { + return getRefs(repository, refs, fullName, maxCount, 0); + } + + /** + * Returns a list of references in the repository matching "refs". If the + * repository is null or empty, an empty list is returned. + * + * @param repository + * @param refs + * if unspecified, all refs are returned + * @param fullName + * if true, /refs/something/yadayadayada is returned. If false, + * yadayadayada is returned. + * @param maxCount + * if < 0, all references are returned + * @param offset + * if maxCount provided sets the starting point of the records to return + * @return list of references + */ + private static List getRefs(Repository repository, String refs, boolean fullName, + int maxCount, int offset) { + List list = new ArrayList(); + if (maxCount == 0) { + return list; + } + if (!hasCommits(repository)) { + return list; + } + try { + Map map = repository.getRefDatabase().getRefs(refs); + RevWalk rw = new RevWalk(repository); + for (Entry entry : map.entrySet()) { + Ref ref = entry.getValue(); + RevObject object = rw.parseAny(ref.getObjectId()); + String name = entry.getKey(); + if (fullName && !StringUtils.isEmpty(refs)) { + name = refs + name; + } + list.add(new RefModel(name, ref, object)); + } + rw.dispose(); + Collections.sort(list); + Collections.reverse(list); + if (maxCount > 0 && list.size() > maxCount) { + if (offset < 0) { + offset = 0; + } + int endIndex = offset + maxCount; + if (endIndex > list.size()) { + endIndex = list.size(); + } + list = new ArrayList(list.subList(offset, endIndex)); + } + } catch (IOException e) { + error(e, repository, "{0} failed to retrieve {1}", refs); + } + return list; + } + + /** + * Returns a RefModel for the gh-pages branch in the repository. If the + * branch can not be found, null is returned. + * + * @param repository + * @return a refmodel for the gh-pages branch or null + */ + public static RefModel getPagesBranch(Repository repository) { + return getBranch(repository, "gh-pages"); + } + + /** + * Returns a RefModel for a specific branch name in the repository. If the + * branch can not be found, null is returned. + * + * @param repository + * @return a refmodel for the branch or null + */ + public static RefModel getBranch(Repository repository, String name) { + RefModel branch = null; + try { + // search for the branch in local heads + for (RefModel ref : JGitUtils.getLocalBranches(repository, false, -1)) { + if (ref.reference.getName().endsWith(name)) { + branch = ref; + break; + } + } + + // search for the branch in remote heads + if (branch == null) { + for (RefModel ref : JGitUtils.getRemoteBranches(repository, false, -1)) { + if (ref.reference.getName().endsWith(name)) { + branch = ref; + break; + } + } + } + } catch (Throwable t) { + LOGGER.error(MessageFormat.format("Failed to find {0} branch!", name), t); + } + return branch; + } + + /** + * Returns the list of submodules for this repository. + * + * @param repository + * @param commit + * @return list of submodules + */ + public static List getSubmodules(Repository repository, String commitId) { + RevCommit commit = getCommit(repository, commitId); + return getSubmodules(repository, commit.getTree()); + } + + /** + * Returns the list of submodules for this repository. + * + * @param repository + * @param commit + * @return list of submodules + */ + public static List getSubmodules(Repository repository, RevTree tree) { + List list = new ArrayList(); + byte [] blob = getByteContent(repository, tree, ".gitmodules", false); + if (blob == null) { + return list; + } + try { + BlobBasedConfig config = new BlobBasedConfig(repository.getConfig(), blob); + for (String module : config.getSubsections("submodule")) { + String path = config.getString("submodule", module, "path"); + String url = config.getString("submodule", module, "url"); + list.add(new SubmoduleModel(module, path, url)); + } + } catch (ConfigInvalidException e) { + LOGGER.error("Failed to load .gitmodules file for " + repository.getDirectory(), e); + } + return list; + } + + /** + * Returns the submodule definition for the specified path at the specified + * commit. If no module is defined for the path, null is returned. + * + * @param repository + * @param commit + * @param path + * @return a submodule definition or null if there is no submodule + */ + public static SubmoduleModel getSubmoduleModel(Repository repository, String commitId, String path) { + for (SubmoduleModel model : getSubmodules(repository, commitId)) { + if (model.path.equals(path)) { + return model; + } + } + return null; + } + + public static String getSubmoduleCommitId(Repository repository, String path, RevCommit commit) { + String commitId = null; + RevWalk rw = new RevWalk(repository); + TreeWalk tw = new TreeWalk(repository); + tw.setFilter(PathFilterGroup.createFromStrings(Collections.singleton(path))); + try { + tw.reset(commit.getTree()); + while (tw.next()) { + if (tw.isSubtree() && !path.equals(tw.getPathString())) { + tw.enterSubtree(); + continue; + } + if (FileMode.GITLINK == tw.getFileMode(0)) { + commitId = tw.getObjectId(0).getName(); + break; + } + } + } catch (Throwable t) { + error(t, repository, "{0} can't find {1} in commit {2}", path, commit.name()); + } finally { + rw.dispose(); + tw.close(); + } + return commitId; + } + + /** + * Returns the list of notes entered about the commit from the refs/notes + * namespace. If the repository does not exist or is empty, an empty list is + * returned. + * + * @param repository + * @param commit + * @return list of notes + */ + public static List getNotesOnCommit(Repository repository, RevCommit commit) { + List list = new ArrayList(); + if (!hasCommits(repository)) { + return list; + } + List noteBranches = getNoteBranches(repository, true, -1); + for (RefModel notesRef : noteBranches) { + RevTree notesTree = JGitUtils.getCommit(repository, notesRef.getName()).getTree(); + // flat notes list + String notePath = commit.getName(); + String text = getStringContent(repository, notesTree, notePath); + if (!StringUtils.isEmpty(text)) { + List history = getRevLog(repository, notesRef.getName(), notePath, 0, -1); + RefModel noteRef = new RefModel(notesRef.displayName, null, history.get(history + .size() - 1)); + GitNote gitNote = new GitNote(noteRef, text); + list.add(gitNote); + continue; + } + + // folder structure + StringBuilder sb = new StringBuilder(commit.getName()); + sb.insert(2, '/'); + notePath = sb.toString(); + text = getStringContent(repository, notesTree, notePath); + if (!StringUtils.isEmpty(text)) { + List history = getRevLog(repository, notesRef.getName(), notePath, 0, -1); + RefModel noteRef = new RefModel(notesRef.displayName, null, history.get(history + .size() - 1)); + GitNote gitNote = new GitNote(noteRef, text); + list.add(gitNote); + } + } + return list; + } + + /** + * this method creates an incremental revision number as a tag according to + * the amount of already existing tags, which start with a defined prefix. + * + * @param repository + * @param objectId + * @param tagger + * @param prefix + * @param intPattern + * @param message + * @return true if operation was successful, otherwise false + */ + public static boolean createIncrementalRevisionTag(Repository repository, + String objectId, PersonIdent tagger, String prefix, String intPattern, String message) { + boolean result = false; + Iterator> iterator = repository.getTags().entrySet().iterator(); + long lastRev = 0; + while (iterator.hasNext()) { + Entry entry = iterator.next(); + if (entry.getKey().startsWith(prefix)) { + try { + long val = Long.parseLong(entry.getKey().substring(prefix.length())); + if (val > lastRev) { + lastRev = val; + } + } catch (Exception e) { + // this tag is NOT an incremental revision tag + } + } + } + DecimalFormat df = new DecimalFormat(intPattern); + result = createTag(repository, objectId, tagger, prefix + df.format((lastRev + 1)), message); + return result; + } + + /** + * creates a tag in a repository + * + * @param repository + * @param objectId, the ref the tag points towards + * @param tagger, the person tagging the object + * @param tag, the string label + * @param message, the string message + * @return boolean, true if operation was successful, otherwise false + */ + public static boolean createTag(Repository repository, String objectId, PersonIdent tagger, String tag, String message) { + try { + Git gitClient = Git.open(repository.getDirectory()); + TagCommand tagCommand = gitClient.tag(); + tagCommand.setTagger(tagger); + tagCommand.setMessage(message); + if (objectId != null) { + RevObject revObj = getCommit(repository, objectId); + tagCommand.setObjectId(revObj); + } + tagCommand.setName(tag); + Ref call = tagCommand.call(); + return call != null ? true : false; + } catch (Exception e) { + error(e, repository, "Failed to create tag {1} in repository {0}", objectId, tag); + } + return false; + } + + /** + * Create an orphaned branch in a repository. + * + * @param repository + * @param branchName + * @param author + * if unspecified, Gitblit will be the author of this new branch + * @return true if successful + */ + public static boolean createOrphanBranch(Repository repository, String branchName, + PersonIdent author) { + boolean success = false; + String message = "Created branch " + branchName; + if (author == null) { + author = new PersonIdent("Gitblit", "gitblit@localhost"); + } + try { + ObjectInserter odi = repository.newObjectInserter(); + try { + // Create a blob object to insert into a tree + ObjectId blobId = odi.insert(Constants.OBJ_BLOB, + message.getBytes(Constants.CHARACTER_ENCODING)); + + // Create a tree object to reference from a commit + TreeFormatter tree = new TreeFormatter(); + tree.append(".branch", FileMode.REGULAR_FILE, blobId); + ObjectId treeId = odi.insert(tree); + + // Create a commit object + CommitBuilder commit = new CommitBuilder(); + commit.setAuthor(author); + commit.setCommitter(author); + commit.setEncoding(Constants.CHARACTER_ENCODING); + commit.setMessage(message); + commit.setTreeId(treeId); + + // Insert the commit into the repository + ObjectId commitId = odi.insert(commit); + odi.flush(); + + RevWalk revWalk = new RevWalk(repository); + try { + RevCommit revCommit = revWalk.parseCommit(commitId); + if (!branchName.startsWith("refs/")) { + branchName = "refs/heads/" + branchName; + } + RefUpdate ru = repository.updateRef(branchName); + ru.setNewObjectId(commitId); + ru.setRefLogMessage("commit: " + revCommit.getShortMessage(), false); + Result rc = ru.forceUpdate(); + switch (rc) { + case NEW: + case FORCED: + case FAST_FORWARD: + success = true; + break; + default: + success = false; + } + } finally { + revWalk.close(); + } + } finally { + odi.close(); + } + } catch (Throwable t) { + error(t, repository, "Failed to create orphan branch {1} in repository {0}", branchName); + } + return success; + } + + /** + * Reads the sparkleshare id, if present, from the repository. + * + * @param repository + * @return an id or null + */ + public static String getSparkleshareId(Repository repository) { + byte[] content = getByteContent(repository, null, ".sparkleshare", false); + if (content == null) { + return null; + } + return StringUtils.decodeString(content); + } + + /** + * Automatic repair of (some) invalid refspecs. These are the result of a + * bug in JGit cloning where a double forward-slash was injected. :( + * + * @param repository + * @return true, if the refspecs were repaired + */ + public static boolean repairFetchSpecs(Repository repository) { + StoredConfig rc = repository.getConfig(); + + // auto-repair broken fetch ref specs + for (String name : rc.getSubsections("remote")) { + int invalidSpecs = 0; + int repairedSpecs = 0; + List specs = new ArrayList(); + for (String spec : rc.getStringList("remote", name, "fetch")) { + try { + RefSpec rs = new RefSpec(spec); + // valid spec + specs.add(spec); + } catch (IllegalArgumentException e) { + // invalid spec + invalidSpecs++; + if (spec.contains("//")) { + // auto-repair this known spec bug + spec = spec.replace("//", "/"); + specs.add(spec); + repairedSpecs++; + } + } + } + + if (invalidSpecs == repairedSpecs && repairedSpecs > 0) { + // the fetch specs were automatically repaired + rc.setStringList("remote", name, "fetch", specs); + try { + rc.save(); + rc.load(); + LOGGER.debug("repaired {} invalid fetch refspecs for {}", repairedSpecs, repository.getDirectory()); + return true; + } catch (Exception e) { + LOGGER.error(null, e); + } + } else if (invalidSpecs > 0) { + LOGGER.error("mirror executor found {} invalid fetch refspecs for {}", invalidSpecs, repository.getDirectory()); + } + } + return false; + } + + /** + * Returns true if the commit identified by commitId is an ancestor or the + * the commit identified by tipId. + * + * @param repository + * @param commitId + * @param tipId + * @return true if there is the commit is an ancestor of the tip + */ + public static boolean isMergedInto(Repository repository, String commitId, String tipId) { + try { + return isMergedInto(repository, repository.resolve(commitId), repository.resolve(tipId)); + } catch (Exception e) { + LOGGER.error("Failed to determine isMergedInto", e); + } + return false; + } + + /** + * Returns true if the commit identified by commitId is an ancestor or the + * the commit identified by tipId. + * + * @param repository + * @param commitId + * @param tipId + * @return true if there is the commit is an ancestor of the tip + */ + public static boolean isMergedInto(Repository repository, ObjectId commitId, ObjectId tipCommitId) { + // traverse the revlog looking for a commit chain between the endpoints + RevWalk rw = new RevWalk(repository); + try { + // must re-lookup RevCommits to workaround undocumented RevWalk bug + RevCommit tip = rw.lookupCommit(tipCommitId); + RevCommit commit = rw.lookupCommit(commitId); + return rw.isMergedInto(commit, tip); + } catch (Exception e) { + LOGGER.error("Failed to determine isMergedInto", e); + } finally { + rw.dispose(); + } + return false; + } + + /** + * Returns the merge base of two commits or null if there is no common + * ancestry. + * + * @param repository + * @param commitIdA + * @param commitIdB + * @return the commit id of the merge base or null if there is no common base + */ + public static String getMergeBase(Repository repository, ObjectId commitIdA, ObjectId commitIdB) { + RevWalk rw = new RevWalk(repository); + try { + RevCommit a = rw.lookupCommit(commitIdA); + RevCommit b = rw.lookupCommit(commitIdB); + + rw.setRevFilter(RevFilter.MERGE_BASE); + rw.markStart(a); + rw.markStart(b); + RevCommit mergeBase = rw.next(); + if (mergeBase == null) { + return null; + } + return mergeBase.getName(); + } catch (Exception e) { + LOGGER.error("Failed to determine merge base", e); + } finally { + rw.dispose(); + } + return null; + } + + public static enum MergeStatus { + MISSING_INTEGRATION_BRANCH, MISSING_SRC_BRANCH, NOT_MERGEABLE, FAILED, ALREADY_MERGED, MERGEABLE, MERGED; + } + + /** + * Determines if we can cleanly merge one branch into another. Returns true + * if we can merge without conflict, otherwise returns false. + * + * @param repository + * @param src + * @param toBranch + * @param mergeType + * Defines the integration strategy to use for merging. + * @return true if we can merge without conflict + */ + public static MergeStatus canMerge(Repository repository, String src, String toBranch, MergeType mergeType) { + IntegrationStrategy strategy = IntegrationStrategyFactory.create(mergeType, repository, src, toBranch); + return strategy.canMerge(); + } + + + public static class MergeResult { + public final MergeStatus status; + public final String sha; + + MergeResult(MergeStatus status, String sha) { + this.status = status; + this.sha = sha; + } + } + + /** + * Tries to merge a commit into a branch. If there are conflicts, the merge + * will fail. + * + * @param repository + * @param src + * @param toBranch + * @param mergeType + * Defines the integration strategy to use for merging. + * @param committer + * @param message + * @return the merge result + */ + public static MergeResult merge(Repository repository, String src, String toBranch, MergeType mergeType, + PersonIdent committer, String message) { + + if (!toBranch.startsWith(Constants.R_REFS)) { + // branch ref doesn't start with ref, assume this is a branch head + toBranch = Constants.R_HEADS + toBranch; + } + + IntegrationStrategy strategy = IntegrationStrategyFactory.create(mergeType, repository, src, toBranch); + MergeResult mergeResult = strategy.merge(committer, message); + + if (mergeResult.status != MergeStatus.MERGED) { + return mergeResult; + } + + try { + // Update the integration branch ref + RefUpdate mergeRefUpdate = repository.updateRef(toBranch); + mergeRefUpdate.setNewObjectId(strategy.getMergeCommit()); + mergeRefUpdate.setRefLogMessage(strategy.getRefLogMessage(), false); + mergeRefUpdate.setExpectedOldObjectId(strategy.branchTip); + RefUpdate.Result rc = mergeRefUpdate.update(); + switch (rc) { + case FAST_FORWARD: + // successful, clean merge + break; + default: + mergeResult = new MergeResult(MergeStatus.FAILED, null); + throw new GitBlitException(MessageFormat.format("Unexpected result \"{0}\" when {1} in {2}", + rc.name(), strategy.getOperationMessage(), repository.getDirectory())); + } + } catch (IOException e) { + LOGGER.error("Failed to merge", e); + } + + return mergeResult; + } + + + private static abstract class IntegrationStrategy { + Repository repository; + String src; + String toBranch; + + RevWalk revWalk; + RevCommit branchTip; + RevCommit srcTip; + + RevCommit mergeCommit; + String refLogMessage; + String operationMessage; + + RevCommit getMergeCommit() { + return mergeCommit; + } + + String getRefLogMessage() { + return refLogMessage; + } + + String getOperationMessage() { + return operationMessage; + } + + IntegrationStrategy(Repository repository, String src, String toBranch) { + this.repository = repository; + this.src = src; + this.toBranch = toBranch; + } + + void prepare() throws IOException { + if (revWalk == null) revWalk = new RevWalk(repository); + ObjectId branchId = repository.resolve(toBranch); + if (branchId != null) { + branchTip = revWalk.lookupCommit(branchId); + } + ObjectId srcId = repository.resolve(src); + if (srcId != null) { + srcTip = revWalk.lookupCommit(srcId); + } + } + + + abstract MergeStatus _canMerge() throws IOException; + + + MergeStatus canMerge() { + try { + prepare(); + if (branchTip == null) { + return MergeStatus.MISSING_INTEGRATION_BRANCH; + } + if (srcTip == null) { + return MergeStatus.MISSING_SRC_BRANCH; + } + if (revWalk.isMergedInto(srcTip, branchTip)) { + // already merged + return MergeStatus.ALREADY_MERGED; + } + // determined by specific integration strategy + return _canMerge(); + + } catch (NullPointerException e) { + LOGGER.error("Failed to determine canMerge", e); + } catch (IOException e) { + LOGGER.error("Failed to determine canMerge", e); + } finally { + if (revWalk != null) { + revWalk.close(); + } + } + + return MergeStatus.NOT_MERGEABLE; + } + + + abstract MergeResult _merge(PersonIdent committer, String message) throws IOException; + + + MergeResult merge(PersonIdent committer, String message) { + try { + prepare(); + if (revWalk.isMergedInto(srcTip, branchTip)) { + // already merged + return new MergeResult(MergeStatus.ALREADY_MERGED, null); + } + // determined by specific integration strategy + return _merge(committer, message); + + } catch (IOException e) { + LOGGER.error("Failed to merge", e); + } finally { + if (revWalk != null) { + revWalk.close(); + } + } + + return new MergeResult(MergeStatus.FAILED, null); + } + } + + + private static class FastForwardOnly extends IntegrationStrategy { + FastForwardOnly(Repository repository, String src, String toBranch) { + super(repository, src, toBranch); + } + + @Override + MergeStatus _canMerge() throws IOException { + if (revWalk.isMergedInto(branchTip, srcTip)) { + // fast-forward + return MergeStatus.MERGEABLE; + } + + return MergeStatus.NOT_MERGEABLE; + } + + @Override + MergeResult _merge(PersonIdent committer, String message) throws IOException { + if (! revWalk.isMergedInto(branchTip, srcTip)) { + // is not fast-forward + return new MergeResult(MergeStatus.FAILED, null); + } + + mergeCommit = srcTip; + refLogMessage = "merge " + src + ": Fast-forward"; + operationMessage = MessageFormat.format("fast-forwarding {0} to commit {1}", srcTip.getName(), branchTip.getName()); + + return new MergeResult(MergeStatus.MERGED, srcTip.getName()); + } + } + + private static class MergeIfNecessary extends IntegrationStrategy { + MergeIfNecessary(Repository repository, String src, String toBranch) { + super(repository, src, toBranch); + } + + @Override + MergeStatus _canMerge() throws IOException { + if (revWalk.isMergedInto(branchTip, srcTip)) { + // fast-forward + return MergeStatus.MERGEABLE; + } + + RecursiveMerger merger = (RecursiveMerger) MergeStrategy.RECURSIVE.newMerger(repository, true); + boolean canMerge = merger.merge(branchTip, srcTip); + if (canMerge) { + return MergeStatus.MERGEABLE; + } + + return MergeStatus.NOT_MERGEABLE; + } + + @Override + MergeResult _merge(PersonIdent committer, String message) throws IOException { + if (revWalk.isMergedInto(branchTip, srcTip)) { + // fast-forward + mergeCommit = srcTip; + refLogMessage = "merge " + src + ": Fast-forward"; + operationMessage = MessageFormat.format("fast-forwarding {0} to commit {1}", branchTip.getName(), srcTip.getName()); + + return new MergeResult(MergeStatus.MERGED, srcTip.getName()); + } + + RecursiveMerger merger = (RecursiveMerger) MergeStrategy.RECURSIVE.newMerger(repository, true); + boolean merged = merger.merge(branchTip, srcTip); + if (merged) { + // create a merge commit and a reference to track the merge commit + ObjectId treeId = merger.getResultTreeId(); + ObjectInserter odi = repository.newObjectInserter(); + try { + // Create a commit object + CommitBuilder commitBuilder = new CommitBuilder(); + commitBuilder.setCommitter(committer); + commitBuilder.setAuthor(committer); + commitBuilder.setEncoding(Constants.CHARSET); + if (StringUtils.isEmpty(message)) { + message = MessageFormat.format("merge {0} into {1}", srcTip.getName(), branchTip.getName()); + } + commitBuilder.setMessage(message); + commitBuilder.setParentIds(branchTip.getId(), srcTip.getId()); + commitBuilder.setTreeId(treeId); + + // Insert the merge commit into the repository + ObjectId mergeCommitId = odi.insert(commitBuilder); + odi.flush(); + + mergeCommit = revWalk.parseCommit(mergeCommitId); + refLogMessage = "commit: " + mergeCommit.getShortMessage(); + operationMessage = MessageFormat.format("merging commit {0} into {1}", srcTip.getName(), branchTip.getName()); + + // return the merge commit id + return new MergeResult(MergeStatus.MERGED, mergeCommitId.getName()); + } finally { + odi.close(); + } + } + return new MergeResult(MergeStatus.FAILED, null); + } + } + + private static class MergeAlways extends IntegrationStrategy { + MergeAlways(Repository repository, String src, String toBranch) { + super(repository, src, toBranch); + } + + @Override + MergeStatus _canMerge() throws IOException { + RecursiveMerger merger = (RecursiveMerger) MergeStrategy.RECURSIVE.newMerger(repository, true); + boolean canMerge = merger.merge(branchTip, srcTip); + if (canMerge) { + return MergeStatus.MERGEABLE; + } + + return MergeStatus.NOT_MERGEABLE; + } + + @Override + MergeResult _merge(PersonIdent committer, String message) throws IOException { + RecursiveMerger merger = (RecursiveMerger) MergeStrategy.RECURSIVE.newMerger(repository, true); + boolean merged = merger.merge(branchTip, srcTip); + if (merged) { + // create a merge commit and a reference to track the merge commit + ObjectId treeId = merger.getResultTreeId(); + ObjectInserter odi = repository.newObjectInserter(); + try { + // Create a commit object + CommitBuilder commitBuilder = new CommitBuilder(); + commitBuilder.setCommitter(committer); + commitBuilder.setAuthor(committer); + commitBuilder.setEncoding(Constants.CHARSET); + if (StringUtils.isEmpty(message)) { + message = MessageFormat.format("merge {0} into {1}", srcTip.getName(), branchTip.getName()); + } + commitBuilder.setMessage(message); + commitBuilder.setParentIds(branchTip.getId(), srcTip.getId()); + commitBuilder.setTreeId(treeId); + + // Insert the merge commit into the repository + ObjectId mergeCommitId = odi.insert(commitBuilder); + odi.flush(); + + mergeCommit = revWalk.parseCommit(mergeCommitId); + refLogMessage = "commit: " + mergeCommit.getShortMessage(); + operationMessage = MessageFormat.format("merging commit {0} into {1}", srcTip.getName(), branchTip.getName()); + + // return the merge commit id + return new MergeResult(MergeStatus.MERGED, mergeCommitId.getName()); + } finally { + odi.close(); + } + } + + return new MergeResult(MergeStatus.FAILED, null); + } + } + + + private static class IntegrationStrategyFactory { + static IntegrationStrategy create(MergeType mergeType, Repository repository, String src, String toBranch) { + switch(mergeType) { + case FAST_FORWARD_ONLY: + return new FastForwardOnly(repository, src, toBranch); + case MERGE_IF_NECESSARY: + return new MergeIfNecessary(repository, src, toBranch); + case MERGE_ALWAYS: + return new MergeAlways(repository, src, toBranch); + } + return null; + } + } + + + /** + * Returns the LFS URL for the given oid + * Currently assumes that the Gitblit Filestore is used + * + * @param baseURL + * @param repository name + * @param oid of lfs item + * @return the lfs item URL + */ + public static String getLfsRepositoryUrl(String baseURL, String repositoryName, String oid) { + + if (baseURL.length() > 0 && baseURL.charAt(baseURL.length() - 1) == '/') { + baseURL = baseURL.substring(0, baseURL.length() - 1); + } + + return baseURL + com.gitblit.Constants.R_PATH + + repositoryName + "/" + + com.gitblit.Constants.R_LFS + + "objects/" + oid; + + } + + /** + * Returns all tree entries that do not match the ignore paths. + * + * @param db + * @param ignorePaths + * @param dcBuilder + * @throws IOException + */ + public static List getTreeEntries(Repository db, String branch, Collection ignorePaths) throws IOException { + List list = new ArrayList(); + TreeWalk tw = null; + try { + ObjectId treeId = db.resolve(branch + "^{tree}"); + if (treeId == null) { + // branch does not exist yet + return list; + } + tw = new TreeWalk(db); + int hIdx = tw.addTree(treeId); + tw.setRecursive(true); + + while (tw.next()) { + String path = tw.getPathString(); + CanonicalTreeParser hTree = null; + if (hIdx != -1) { + hTree = tw.getTree(hIdx, CanonicalTreeParser.class); + } + if (!ignorePaths.contains(path)) { + // add all other tree entries + if (hTree != null) { + final DirCacheEntry entry = new DirCacheEntry(path); + entry.setObjectId(hTree.getEntryObjectId()); + entry.setFileMode(hTree.getEntryFileMode()); + list.add(entry); + } + } + } + } finally { + if (tw != null) { + tw.close(); + } + } + return list; + } + + public static boolean commitIndex(Repository db, String branch, DirCache index, + ObjectId parentId, boolean forceCommit, + String author, String authorEmail, String message) throws IOException, ConcurrentRefUpdateException { + boolean success = false; + + ObjectId headId = db.resolve(branch + "^{commit}"); + ObjectId baseId = parentId; + if (baseId == null || headId == null) { return false; } + + ObjectInserter odi = db.newObjectInserter(); + try { + // Create the in-memory index of the new/updated ticket + ObjectId indexTreeId = index.writeTree(odi); + + // Create a commit object + PersonIdent ident = new PersonIdent(author, authorEmail); + + if (forceCommit == false) { + ThreeWayMerger merger = MergeStrategy.RECURSIVE.newMerger(db, true); + merger.setObjectInserter(odi); + merger.setBase(baseId); + boolean mergeSuccess = merger.merge(indexTreeId, headId); + + if (mergeSuccess) { + indexTreeId = merger.getResultTreeId(); + } else { + //Manual merge required + return false; + } + } + + CommitBuilder commit = new CommitBuilder(); + commit.setAuthor(ident); + commit.setCommitter(ident); + commit.setEncoding(com.gitblit.Constants.ENCODING); + commit.setMessage(message); + commit.setParentId(headId); + commit.setTreeId(indexTreeId); + + // Insert the commit into the repository + ObjectId commitId = odi.insert(commit); + odi.flush(); + + RevWalk revWalk = new RevWalk(db); + try { + RevCommit revCommit = revWalk.parseCommit(commitId); + RefUpdate ru = db.updateRef(branch); + ru.setForceUpdate(forceCommit); + ru.setNewObjectId(commitId); + ru.setExpectedOldObjectId(headId); + ru.setRefLogMessage("commit: " + revCommit.getShortMessage(), false); + Result rc = ru.update(); + + switch (rc) { + case NEW: + case FORCED: + case FAST_FORWARD: + success = true; + break; + case REJECTED: + case LOCK_FAILURE: + throw new ConcurrentRefUpdateException(JGitText.get().couldNotLockHEAD, + ru.getRef(), rc); + default: + throw new JGitInternalException(MessageFormat.format( + JGitText.get().updatingRefFailed, branch, commitId.toString(), + rc)); + } + } finally { + revWalk.close(); + } + } finally { + odi.close(); + } + return success; + } + + /** + * Returns true if the commit identified by commitId is at the tip of it's branch. + * + * @param repository + * @param commitId + * @return true if the given commit is the tip + */ + public static boolean isTip(Repository repository, String commitId) { + try { + RefModel tip = getBranch(repository, commitId); + return (tip != null); + } catch (Exception e) { + LOGGER.error("Failed to determine isTip", e); + } + return false; + } + + /* + * Identify ticket by considering the branch the commit is on + * + * @param repository + * @param commit + * @return ticket number, or 0 if no ticket + */ + public static long getTicketNumberFromCommitBranch(Repository repository, RevCommit commit) { + // try lookup by change ref + Map> map = repository.getAllRefsByPeeledObjectId(); + Set refs = map.get(commit.getId()); + if (!ArrayUtils.isEmpty(refs)) { + for (Ref ref : refs) { + long number = PatchsetCommand.getTicketNumber(ref.getName()); + + if (number > 0) { + return number; + } + } + } + + return 0; + } + + + /** + * Try to identify all referenced tickets from the commit. + * + * @param commit + * @return a collection of TicketLinks + */ + @NotNull + public static List identifyTicketsFromCommitMessage(Repository repository, IStoredSettings settings, + RevCommit commit) { + List ticketLinks = new ArrayList(); + List linkedTickets = new ArrayList(); + + // parse commit message looking for fixes/closes #n + final String xFixDefault = "(?:fixes|closes)[\\s-]+#?(\\d+)"; + String xFix = settings.getString(Keys.tickets.closeOnPushCommitMessageRegex, xFixDefault); + if (StringUtils.isEmpty(xFix)) { + xFix = xFixDefault; + } + try { + Pattern p = Pattern.compile(xFix, Pattern.CASE_INSENSITIVE); + Matcher m = p.matcher(commit.getFullMessage()); + while (m.find()) { + String val = m.group(1); + long number = Long.parseLong(val); + + if (number > 0) { + ticketLinks.add(new TicketLink(number, TicketAction.Close)); + linkedTickets.add(number); + } + } + } catch (Exception e) { + LOGGER.error(String.format("Failed to parse \"%s\" in commit %s", xFix, commit.getName()), e); + } + + // parse commit message looking for ref #n + final String xRefDefault = "(?:ref|task|issue|bug)?[\\s-]*#(\\d+)"; + String xRef = settings.getString(Keys.tickets.linkOnPushCommitMessageRegex, xRefDefault); + if (StringUtils.isEmpty(xRef)) { + xRef = xRefDefault; + } + try { + Pattern p = Pattern.compile(xRef, Pattern.CASE_INSENSITIVE); + Matcher m = p.matcher(commit.getFullMessage()); + while (m.find()) { + String val = m.group(1); + long number = Long.parseLong(val); + //Most generic case so don't included tickets more precisely linked + if ((number > 0) && (!linkedTickets.contains(number))) { + ticketLinks.add( new TicketLink(number, TicketAction.Commit, commit.getName())); + linkedTickets.add(number); + } + } + } catch (Exception e) { + LOGGER.error(String.format("Failed to parse \"%s\" in commit %s", xRef, commit.getName()), e); + } + + return ticketLinks; + } + + /** + * Try to identify all referenced tickets between two commits + * + * @param commit + * @param parseMessage + * @param currentTicketId, or 0 if not on a ticket branch + * @return a collection of TicketLink, or null if commit is already linked + */ + public static List identifyTicketsBetweenCommits(Repository repository, IStoredSettings settings, + String baseSha, String tipSha) { + List links = new ArrayList(); + if (repository == null) { return links; } + + RevWalk walk = new RevWalk(repository); + walk.sort(RevSort.TOPO); + walk.sort(RevSort.REVERSE, true); + try { + RevCommit tip = walk.parseCommit(repository.resolve(tipSha)); + RevCommit base = walk.parseCommit(repository.resolve(baseSha)); + walk.markStart(tip); + walk.markUninteresting(base); + for (;;) { + RevCommit commit = walk.next(); + if (commit == null) { + break; + } + links.addAll(JGitUtils.identifyTicketsFromCommitMessage(repository, settings, commit)); + } + } catch (IOException e) { + LOGGER.error("failed to identify tickets between commits.", e); + } finally { + walk.dispose(); + } + + return links; + } + + public static int countCommits(Repository repository, RevWalk walk, ObjectId baseId, ObjectId tipId) { + int count = 0; + walk.reset(); + walk.sort(RevSort.TOPO); + walk.sort(RevSort.REVERSE, true); + try { + RevCommit tip = walk.parseCommit(tipId); + RevCommit base = walk.parseCommit(baseId); + walk.markStart(tip); + walk.markUninteresting(base); + for (;;) { + RevCommit c = walk.next(); + if (c == null) { + break; + } + count++; + } + } catch (IOException e) { + // Should never happen, the core receive process would have + // identified the missing object earlier before we got control. + LOGGER.error("failed to get commit count", e); + return 0; + } finally { + walk.close(); + } + return count; + } + + public static int countCommits(Repository repository, RevWalk walk, String baseId, String tipId) { + int count = 0; + try { + count = countCommits(repository, walk, repository.resolve(baseId),repository.resolve(tipId)); + } catch (IOException e) { + LOGGER.error("failed to get commit count", e); + } + return count; + } +} From a53a5e9ada58c13be42245b193a64b8ae9a7307d Mon Sep 17 00:00:00 2001 From: Tomasz Sztejka Date: Wed, 3 Nov 2021 14:20:00 +0100 Subject: [PATCH 14/21] Moved logging system initialization to static code to ensure, at best possible level, that utils classes static initializers are NOT initialized before logging is set up. --- .gitignore | 1 + src/main/java/com/gitblit/GitBlitServer.java | 78 +- .../java/com/gitblit/utils/#JGitUtils.java# | 3122 ----------------- 3 files changed, 40 insertions(+), 3161 deletions(-) delete mode 100644 src/main/java/com/gitblit/utils/#JGitUtils.java# diff --git a/.gitignore b/.gitignore index f2772ee42..c02c62637 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ tags +\#*\# /temp /lib /ext diff --git a/src/main/java/com/gitblit/GitBlitServer.java b/src/main/java/com/gitblit/GitBlitServer.java index 830c3c242..a0bd9523e 100644 --- a/src/main/java/com/gitblit/GitBlitServer.java +++ b/src/main/java/com/gitblit/GitBlitServer.java @@ -83,29 +83,11 @@ */ public class GitBlitServer { - /* Design notes: - The logger initialization in classes along this application must - have a well behaved policy and either use static initialization: - - static final Logger logger =... - - or dynamic, per instance - - final Logger logger = .... - - Since I did observe that both policies are mixed server MUST - initialize all logging facilities BEFORE any other class is - REFERENCED (by touching any field or etc.). If it is done - differently there is a possibility that static loaders will - pick-up loggers BEFORE server will have a chance to initialize - static field. - */ - private static final Logger logger; + private static Logger logger; + public static void main(String... args) { - GitBlitServer server = new GitBlitServer(); - // filter out the baseFolder parameter List filtered = new ArrayList(); String folder = "data"; @@ -123,23 +105,44 @@ public static void main(String... args) { filtered.add(arg); } } - + //parse parameters Params.baseFolder = folder; Params params = new Params(); CmdLineParser parser = new CmdLineParser(params); try { parser.parseArgument(filtered); if (params.help) { - server.usage(parser, null); + usage(parser, null); } } catch (CmdLineException t) { - server.usage(parser, t); + usage(parser, t); } - + + if (params.stop) { - server.stop(params); + stop(params); } else { - server.start(params); + //Set up log services BEFORE any class which may use logs is touched. + //The logging system initialization in classes along this application + // uses mixed policy: + // static final Logger logger =... + // or dynamic, per instance + // final Logger logger = .... + // So we need to configure logging as quickly as possible so that + // class loaders would not create misconfigured loggers. + + //Load file settings, since pieces of them do configure logging. + FileSettings settings = params.FILESETTINGS; + if (!StringUtils.isEmpty(params.settingsfile)) { + if (new File(params.settingsfile).exists()) { + settings = new FileSettings(params.settingsfile); + } + } + //Set up logging. + setUpLogging(settings, params); + + GitBlitServer server = new GitBlitServer(); + server.start(params, settings); } } @@ -149,7 +152,7 @@ public static void main(String... args) { * @param parser * @param t */ - protected final void usage(CmdLineParser parser, CmdLineException t) { + protected final static void usage(CmdLineParser parser, CmdLineException t) { System.out.println(Constants.BORDER); System.out.println(Constants.getGitBlitVersion()); System.out.println(Constants.BORDER); @@ -166,7 +169,7 @@ protected final void usage(CmdLineParser parser, CmdLineException t) { System.exit(0); } - protected File getBaseFolder(Params params) { + protected static File getBaseFolder(Params params) { String path = System.getProperty("GITBLIT_HOME", Params.baseFolder); if (!StringUtils.isEmpty(System.getenv("GITBLIT_HOME"))) { path = System.getenv("GITBLIT_HOME"); @@ -178,7 +181,7 @@ protected File getBaseFolder(Params params) { /** * Stop Gitblt GO. */ - public void stop(Params params) { + public static void stop(Params params) { try { Socket s = new Socket(InetAddress.getByName("127.0.0.1"), params.shutdownPort); OutputStream out = s.getOutputStream(); @@ -195,15 +198,12 @@ public void stop(Params params) { /** * Start Gitblit GO. + @param params command line parameters, parsed + @param settings loaded settings from properties fiels. */ - protected final void start(Params params) { + protected final void start(Params params, FileSettings settings) { final File baseFolder = getBaseFolder(params); - FileSettings settings = params.FILESETTINGS; - if (!StringUtils.isEmpty(params.settingsfile)) { - if (new File(params.settingsfile).exists()) { - settings = new FileSettings(params.settingsfile); - } - } + /* -------- set up logging ---------*/ setUpLogging(settings,params); @@ -473,7 +473,7 @@ protected GitblitContext newGitblit(IStoredSettings settings, File baseFolder) { private boolean isWindows() { return System.getProperty("os.name").toLowerCase().indexOf("windows") > -1; } - + /** A routine responsible for setting up log4j/slf4j logging environment. This method should be called inside {@link #start} @@ -497,7 +497,7 @@ private boolean isWindows() { @param settings content of "default.properties" file and/or it's overrides. @param params parsed command-line parameters */ - private void setUpLogging(final FileSettings settings, final Params params) + private static void setUpLogging(final FileSettings settings, final Params params) { final File baseFolder = getBaseFolder(params); //Note: @@ -531,7 +531,7 @@ private void setUpLogging(final FileSettings settings, final Params params) { InputStream is = null; try{ - is = getClass().getResourceAsStream("/log4j.properties"); + is = GitBlitServer.class.getResourceAsStream("/log4j.properties"); assert(is!=null); //this resource is always present. loggingProperties.load(is); }catch (Exception e) diff --git a/src/main/java/com/gitblit/utils/#JGitUtils.java# b/src/main/java/com/gitblit/utils/#JGitUtils.java# deleted file mode 100644 index e70b4f996..000000000 --- a/src/main/java/com/gitblit/utils/#JGitUtils.java# +++ /dev/null @@ -1,3122 +0,0 @@ -/* - * Copyright 2011 gitblit.com. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.gitblit.utils; - -import java.io.File; -import java.io.IOException; -import java.text.DecimalFormat; -import java.text.MessageFormat; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collection; -import java.util.Collections; -import java.util.Date; -import java.util.HashMap; -import java.util.Iterator; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.Map.Entry; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -import org.apache.commons.io.filefilter.TrueFileFilter; -import org.eclipse.jgit.api.CloneCommand; -import org.eclipse.jgit.api.FetchCommand; -import org.eclipse.jgit.api.Git; -import org.eclipse.jgit.api.TagCommand; -import org.eclipse.jgit.api.errors.ConcurrentRefUpdateException; -import org.eclipse.jgit.api.errors.GitAPIException; -import org.eclipse.jgit.api.errors.JGitInternalException; -import org.eclipse.jgit.diff.DiffEntry; -import org.eclipse.jgit.diff.DiffEntry.ChangeType; -import org.eclipse.jgit.diff.DiffFormatter; -import org.eclipse.jgit.diff.RawTextComparator; -import org.eclipse.jgit.dircache.DirCache; -import org.eclipse.jgit.dircache.DirCacheEntry; -import org.eclipse.jgit.errors.AmbiguousObjectException; -import org.eclipse.jgit.errors.ConfigInvalidException; -import org.eclipse.jgit.errors.IncorrectObjectTypeException; -import org.eclipse.jgit.errors.LargeObjectException; -import org.eclipse.jgit.errors.MissingObjectException; -import org.eclipse.jgit.errors.RevisionSyntaxException; -import org.eclipse.jgit.errors.StopWalkException; -import org.eclipse.jgit.internal.JGitText; -import org.eclipse.jgit.lib.AnyObjectId; -import org.eclipse.jgit.lib.BlobBasedConfig; -import org.eclipse.jgit.lib.CommitBuilder; -import org.eclipse.jgit.lib.Constants; -import org.eclipse.jgit.lib.FileMode; -import org.eclipse.jgit.lib.ObjectId; -import org.eclipse.jgit.lib.ObjectInserter; -import org.eclipse.jgit.lib.ObjectLoader; -import org.eclipse.jgit.lib.PersonIdent; -import org.eclipse.jgit.lib.Ref; -import org.eclipse.jgit.lib.RefUpdate; -import org.eclipse.jgit.lib.RefUpdate.Result; -import org.eclipse.jgit.lib.Repository; -import org.eclipse.jgit.lib.RepositoryCache.FileKey; -import org.eclipse.jgit.lib.StoredConfig; -import org.eclipse.jgit.lib.TreeFormatter; -import org.eclipse.jgit.merge.MergeStrategy; -import org.eclipse.jgit.merge.RecursiveMerger; -import org.eclipse.jgit.merge.ThreeWayMerger; -import org.eclipse.jgit.revwalk.RevBlob; -import org.eclipse.jgit.revwalk.RevCommit; -import org.eclipse.jgit.revwalk.RevObject; -import org.eclipse.jgit.revwalk.RevSort; -import org.eclipse.jgit.revwalk.RevTree; -import org.eclipse.jgit.revwalk.RevWalk; -import org.eclipse.jgit.revwalk.filter.CommitTimeRevFilter; -import org.eclipse.jgit.revwalk.filter.RevFilter; -import org.eclipse.jgit.storage.file.FileRepositoryBuilder; -import org.eclipse.jgit.transport.CredentialsProvider; -import org.eclipse.jgit.transport.FetchResult; -import org.eclipse.jgit.transport.RefSpec; -import org.eclipse.jgit.treewalk.CanonicalTreeParser; -import org.eclipse.jgit.treewalk.TreeWalk; -import org.eclipse.jgit.treewalk.filter.AndTreeFilter; -import org.eclipse.jgit.treewalk.filter.OrTreeFilter; -import org.eclipse.jgit.treewalk.filter.PathFilter; -import org.eclipse.jgit.treewalk.filter.PathFilterGroup; -import org.eclipse.jgit.treewalk.filter.PathSuffixFilter; -import org.eclipse.jgit.treewalk.filter.TreeFilter; -import org.eclipse.jgit.util.FS; -import org.jetbrains.annotations.NotNull; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import com.gitblit.Constants.MergeType; -import com.gitblit.GitBlitException; -import com.gitblit.IStoredSettings; -import com.gitblit.Keys; -import com.gitblit.git.PatchsetCommand; -import com.gitblit.models.FilestoreModel; -import com.gitblit.models.GitNote; -import com.gitblit.models.PathModel; -import com.gitblit.models.PathModel.PathChangeModel; -import com.gitblit.models.TicketModel.TicketAction; -import com.gitblit.models.TicketModel.TicketLink; -import com.gitblit.models.RefModel; -import com.gitblit.models.SubmoduleModel; -import com.google.common.base.Strings; - -/** - * Collection of static methods for retrieving information from a repository. - * - * @author James Moger - * - */ -public class JGitUtils { - - static final Logger LOGGER = LoggerFactory.getLogger(JGitUtils.class); - - /** - * Log an error message and exception. - * - * @param t - * @param repository - * if repository is not null it MUST be the {0} parameter in the - * pattern. - * @param pattern - * @param objects - */ - private static void error(Throwable t, Repository repository, String pattern, Object... objects) { - List parameters = new ArrayList(); - if (objects != null && objects.length > 0) { - for (Object o : objects) { - parameters.add(o); - } - } - if (repository != null) { - parameters.add(0, repository.getDirectory().getAbsolutePath()); - } - LOGGER.error(MessageFormat.format(pattern, parameters.toArray()), t); - } - - /** - * Returns the displayable name of the person in the form "Real Name ". If the email address is empty, just "Real Name" is returned. - * - * @param person - * @return "Real Name " or "Real Name" - */ - public static String getDisplayName(PersonIdent person) { - if (StringUtils.isEmpty(person.getEmailAddress())) { - return person.getName(); - } - final StringBuilder r = new StringBuilder(); - r.append(person.getName()); - r.append(" <"); - r.append(person.getEmailAddress()); - r.append('>'); - return r.toString().trim(); - } - - /** - * Encapsulates the result of cloning or pulling from a repository. - */ - public static class CloneResult { - public String name; - public FetchResult fetchResult; - public boolean createdRepository; - } - - /** - * Clone or Fetch a repository. If the local repository does not exist, - * clone is called. If the repository does exist, fetch is called. By - * default the clone/fetch retrieves the remote heads, tags, and notes. - * - * @param repositoriesFolder - * @param name - * @param fromUrl - * @return CloneResult - * @throws Exception - */ - public static CloneResult cloneRepository(File repositoriesFolder, String name, String fromUrl) - throws Exception { - return cloneRepository(repositoriesFolder, name, fromUrl, true, null); - } - - /** - * Clone or Fetch a repository. If the local repository does not exist, - * clone is called. If the repository does exist, fetch is called. By - * default the clone/fetch retrieves the remote heads, tags, and notes. - * - * @param repositoriesFolder - * @param name - * @param fromUrl - * @param bare - * @param credentialsProvider - * @return CloneResult - * @throws Exception - */ - public static CloneResult cloneRepository(File repositoriesFolder, String name, String fromUrl, - boolean bare, CredentialsProvider credentialsProvider) throws Exception { - CloneResult result = new CloneResult(); - if (bare) { - // bare repository, ensure .git suffix - if (!name.toLowerCase().endsWith(Constants.DOT_GIT_EXT)) { - name += Constants.DOT_GIT_EXT; - } - } else { - // normal repository, strip .git suffix - if (name.toLowerCase().endsWith(Constants.DOT_GIT_EXT)) { - name = name.substring(0, name.indexOf(Constants.DOT_GIT_EXT)); - } - } - result.name = name; - - File folder = new File(repositoriesFolder, name); - if (folder.exists()) { - File gitDir = FileKey.resolve(new File(repositoriesFolder, name), FS.DETECTED); - Repository repository = new FileRepositoryBuilder().setGitDir(gitDir).build(); - result.fetchResult = fetchRepository(credentialsProvider, repository); - repository.close(); - } else { - CloneCommand clone = new CloneCommand(); - clone.setBare(bare); - clone.setCloneAllBranches(true); - clone.setURI(fromUrl); - clone.setDirectory(folder); - if (credentialsProvider != null) { - clone.setCredentialsProvider(credentialsProvider); - } - Repository repository = clone.call().getRepository(); - - // Now we have to fetch because CloneCommand doesn't fetch - // refs/notes nor does it allow manual RefSpec. - result.createdRepository = true; - result.fetchResult = fetchRepository(credentialsProvider, repository); - repository.close(); - } - return result; - } - - /** - * Fetch updates from the remote repository. If refSpecs is unspecifed, - * remote heads, tags, and notes are retrieved. - * - * @param credentialsProvider - * @param repository - * @param refSpecs - * @return FetchResult - * @throws Exception - */ - public static FetchResult fetchRepository(CredentialsProvider credentialsProvider, - Repository repository, RefSpec... refSpecs) throws Exception { - Git git = new Git(repository); - FetchCommand fetch = git.fetch(); - List specs = new ArrayList(); - if (refSpecs == null || refSpecs.length == 0) { - specs.add(new RefSpec("+refs/heads/*:refs/remotes/origin/*")); - specs.add(new RefSpec("+refs/tags/*:refs/tags/*")); - specs.add(new RefSpec("+refs/notes/*:refs/notes/*")); - } else { - specs.addAll(Arrays.asList(refSpecs)); - } - if (credentialsProvider != null) { - fetch.setCredentialsProvider(credentialsProvider); - } - fetch.setRefSpecs(specs); - FetchResult fetchRes = fetch.call(); - return fetchRes; - } - - /** - * Creates a bare repository. - * - * @param repositoriesFolder - * @param name - * @return Repository - */ - public static Repository createRepository(File repositoriesFolder, String name) { - return createRepository(repositoriesFolder, name, "FALSE"); - } - - /** - * Creates a bare, shared repository. - * - * @param repositoriesFolder - * @param name - * @param shared - * the setting for the --shared option of "git init". - * @return Repository - */ - public static Repository createRepository(File repositoriesFolder, String name, String shared) { - try { - Repository repo = null; - try { - Git git = Git.init().setDirectory(new File(repositoriesFolder, name)).setBare(true).call(); - repo = git.getRepository(); - } catch (GitAPIException e) { - throw new RuntimeException(e); - } - - GitConfigSharedRepository sharedRepository = new GitConfigSharedRepository(shared); - if (sharedRepository.isShared()) { - StoredConfig config = repo.getConfig(); - config.setString("core", null, "sharedRepository", sharedRepository.getValue()); - config.setBoolean("receive", null, "denyNonFastforwards", true); - config.save(); - - if (! JnaUtils.isWindows()) { - Iterator iter = org.apache.commons.io.FileUtils.iterateFilesAndDirs(repo.getDirectory(), - TrueFileFilter.INSTANCE, TrueFileFilter.INSTANCE); - // Adjust permissions on file/directory - while (iter.hasNext()) { - adjustSharedPerm(iter.next(), sharedRepository); - } - } - } - - return repo; - } catch (IOException e) { - throw new RuntimeException(e); - } - } - - private enum GitConfigSharedRepositoryValue - { - UMASK("0", 0), FALSE("0", 0), OFF("0", 0), NO("0", 0), - GROUP("1", 0660), TRUE("1", 0660), ON("1", 0660), YES("1", 0660), - ALL("2", 0664), WORLD("2", 0664), EVERYBODY("2", 0664), - Oxxx(null, -1); - - private String configValue; - private int permValue; - private GitConfigSharedRepositoryValue(String config, int perm) { configValue = config; permValue = perm; }; - - public String getConfigValue() { return configValue; }; - public int getPerm() { return permValue; }; - - } - - private static class GitConfigSharedRepository - { - private int intValue; - private GitConfigSharedRepositoryValue enumValue; - - GitConfigSharedRepository(String s) { - if ( s == null || s.trim().isEmpty() ) { - enumValue = GitConfigSharedRepositoryValue.GROUP; - } - else { - try { - // Try one of the string values - enumValue = GitConfigSharedRepositoryValue.valueOf(s.trim().toUpperCase()); - } catch (IllegalArgumentException iae) { - try { - // Try if this is an octal number - int i = Integer.parseInt(s, 8); - if ( (i & 0600) != 0600 ) { - String msg = String.format("Problem with core.sharedRepository filemode value (0%03o).\nThe owner of files must always have read and write permissions.", i); - throw new IllegalArgumentException(msg); - } - intValue = i & 0666; - enumValue = GitConfigSharedRepositoryValue.Oxxx; - } catch (NumberFormatException nfe) { - throw new IllegalArgumentException("Bad configuration value for 'shared': '" + s + "'"); - } - } - } - } - - String getValue() { - if ( enumValue == GitConfigSharedRepositoryValue.Oxxx ) { - if (intValue == 0) return "0"; - return String.format("0%o", intValue); - } - return enumValue.getConfigValue(); - } - - int getPerm() { - if ( enumValue == GitConfigSharedRepositoryValue.Oxxx ) return intValue; - return enumValue.getPerm(); - } - - boolean isCustom() { - return enumValue == GitConfigSharedRepositoryValue.Oxxx; - } - - boolean isShared() { - return (enumValue.getPerm() > 0) || enumValue == GitConfigSharedRepositoryValue.Oxxx; - } - } - - - /** - * Adjust file permissions of a file/directory for shared repositories - * - * @param path - * File that should get its permissions changed. - * @param configShared - * Configuration string value for the shared mode. - * @return Upon successful completion, a value of 0 is returned. Otherwise, a value of -1 is returned. - */ - public static int adjustSharedPerm(File path, String configShared) { - return adjustSharedPerm(path, new GitConfigSharedRepository(configShared)); - } - - - /** - * Adjust file permissions of a file/directory for shared repositories - * - * @param path - * File that should get its permissions changed. - * @param configShared - * Configuration setting for the shared mode. - * @return Upon successful completion, a value of 0 is returned. Otherwise, a value of -1 is returned. - */ - public static int adjustSharedPerm(File path, GitConfigSharedRepository configShared) { - if (! configShared.isShared()) return 0; - if (! path.exists()) return -1; - - int perm = configShared.getPerm(); - JnaUtils.Filestat stat = JnaUtils.getFilestat(path); - if (stat == null) return -1; - int mode = stat.mode; - if (mode < 0) return -1; - - // Now, here is the kicker: Under Linux, chmod'ing a sgid file whose guid is different from the process' - // effective guid will reset the sgid flag of the file. Since there is no way to get the sgid flag back in - // that case, we decide to rather not touch is and getting the right permissions will have to be achieved - // in a different way, e.g. by using an appropriate umask for the Gitblit process. - if (System.getProperty("os.name").toLowerCase().startsWith("linux")) { - if ( ((mode & (JnaUtils.S_ISGID | JnaUtils.S_ISUID)) != 0) - && stat.gid != JnaUtils.getegid() ) { - LOGGER.debug("Not adjusting permissions to prevent clearing suid/sgid bits for '" + path + "'" ); - return 0; - } - } - - // If the owner has no write access, delete it from group and other, too. - if ((mode & JnaUtils.S_IWUSR) == 0) perm &= ~0222; - // If the owner has execute access, set it for all blocks that have read access. - if ((mode & JnaUtils.S_IXUSR) == JnaUtils.S_IXUSR) perm |= (perm & 0444) >> 2; - - if (configShared.isCustom()) { - // Use the custom value for access permissions. - mode = (mode & ~0777) | perm; - } - else { - // Just add necessary bits to existing permissions. - mode |= perm; - } - - if (path.isDirectory()) { - mode |= (mode & 0444) >> 2; - mode |= JnaUtils.S_ISGID; - } - - return JnaUtils.setFilemode(path, mode); - } - - - /** - * Returns a list of repository names in the specified folder. - * - * @param repositoriesFolder - * @param onlyBare - * if true, only bare repositories repositories are listed. If - * false all repositories are included. - * @param searchSubfolders - * recurse into subfolders to find grouped repositories - * @param depth - * optional recursion depth, -1 = infinite recursion - * @param exclusions - * list of regex exclusions for matching to folder names - * @return list of repository names - */ - public static List getRepositoryList(File repositoriesFolder, boolean onlyBare, - boolean searchSubfolders, int depth, List exclusions) { - List list = new ArrayList(); - if (repositoriesFolder == null || !repositoriesFolder.exists()) { - return list; - } - List patterns = new ArrayList(); - if (!ArrayUtils.isEmpty(exclusions)) { - for (String regex : exclusions) { - patterns.add(Pattern.compile(regex)); - } - } - list.addAll(getRepositoryList(repositoriesFolder.getAbsolutePath(), repositoriesFolder, - onlyBare, searchSubfolders, depth, patterns)); - StringUtils.sortRepositorynames(list); - list.remove(".git"); // issue-256 - return list; - } - - /** - * Recursive function to find git repositories. - * - * @param basePath - * basePath is stripped from the repository name as repositories - * are relative to this path - * @param searchFolder - * @param onlyBare - * if true only bare repositories will be listed. if false all - * repositories are included. - * @param searchSubfolders - * recurse into subfolders to find grouped repositories - * @param depth - * recursion depth, -1 = infinite recursion - * @param patterns - * list of regex patterns for matching to folder names - * @return - */ - private static List getRepositoryList(String basePath, File searchFolder, - boolean onlyBare, boolean searchSubfolders, int depth, List patterns) { - File baseFile = new File(basePath); - List list = new ArrayList(); - if (depth == 0) { - return list; - } - - int nextDepth = (depth == -1) ? -1 : depth - 1; - for (File file : searchFolder.listFiles()) { - if (file.isDirectory()) { - boolean exclude = false; - for (Pattern pattern : patterns) { - String path = FileUtils.getRelativePath(baseFile, file).replace('\\', '/'); - if (pattern.matcher(path).matches()) { - LOGGER.debug(MessageFormat.format("excluding {0} because of rule {1}", path, pattern.pattern())); - exclude = true; - break; - } - } - if (exclude) { - // skip to next file - continue; - } - - File gitDir = FileKey.resolve(new File(searchFolder, file.getName()), FS.DETECTED); - if (gitDir != null) { - if (onlyBare && gitDir.getName().equals(".git")) { - continue; - } - if (gitDir.equals(file) || gitDir.getParentFile().equals(file)) { - // determine repository name relative to base path - String repository = FileUtils.getRelativePath(baseFile, file); - list.add(repository); - } else if (searchSubfolders && file.canRead()) { - // look for repositories in subfolders - list.addAll(getRepositoryList(basePath, file, onlyBare, searchSubfolders, - nextDepth, patterns)); - } - } else if (searchSubfolders && file.canRead()) { - // look for repositories in subfolders - list.addAll(getRepositoryList(basePath, file, onlyBare, searchSubfolders, - nextDepth, patterns)); - } - } - } - return list; - } - - /** - * Returns the first commit on a branch. If the repository does not exist or - * is empty, null is returned. - * - * @param repository - * @param branch - * if unspecified, HEAD is assumed. - * @return RevCommit - */ - public static RevCommit getFirstCommit(Repository repository, String branch) { - if (!hasCommits(repository)) { - return null; - } - RevCommit commit = null; - try { - // resolve branch - ObjectId branchObject; - if (StringUtils.isEmpty(branch)) { - branchObject = getDefaultBranch(repository); - } else { - branchObject = repository.resolve(branch); - } - - RevWalk walk = new RevWalk(repository); - walk.sort(RevSort.REVERSE); - RevCommit head = walk.parseCommit(branchObject); - walk.markStart(head); - commit = walk.next(); - walk.dispose(); - } catch (Throwable t) { - error(t, repository, "{0} failed to determine first commit"); - } - return commit; - } - - /** - * Returns the date of the first commit on a branch. If the repository does - * not exist, Date(0) is returned. If the repository does exist bit is - * empty, the last modified date of the repository folder is returned. - * - * @param repository - * @param branch - * if unspecified, HEAD is assumed. - * @return Date of the first commit on a branch - */ - public static Date getFirstChange(Repository repository, String branch) { - RevCommit commit = getFirstCommit(repository, branch); - if (commit == null) { - if (repository == null || !repository.getDirectory().exists()) { - return new Date(0); - } - // fresh repository - return new Date(repository.getDirectory().lastModified()); - } - return getCommitDate(commit); - } - - /** - * Determine if a repository has any commits. This is determined by checking - * the for loose and packed objects. - * - * @param repository - * @return true if the repository has commits - */ - public static boolean hasCommits(Repository repository) { - if (repository != null && repository.getDirectory().exists()) { - return (new File(repository.getDirectory(), "objects").list().length > 2) - || (new File(repository.getDirectory(), "objects/pack").list().length > 0); - } - return false; - } - - /** - * Encapsulates the result of cloning or pulling from a repository. - */ - public static class LastChange { - public Date when; - public String who; - - LastChange() { - when = new Date(0); - } - - LastChange(long lastModified) { - this.when = new Date(lastModified); - } - } - - /** - * Returns the date and author of the most recent commit on a branch. If the - * repository does not exist Date(0) is returned. If it does exist but is - * empty, the last modified date of the repository folder is returned. - * - * @param repository - * @return a LastChange object - */ - public static LastChange getLastChange(Repository repository) { - if (!hasCommits(repository)) { - // null repository - if (repository == null) { - return new LastChange(); - } - // fresh repository - return new LastChange(repository.getDirectory().lastModified()); - } - - List branchModels = getLocalBranches(repository, true, -1); - if (branchModels.size() > 0) { - // find most recent branch update - LastChange lastChange = new LastChange(); - for (RefModel branchModel : branchModels) { - if (branchModel.getDate().after(lastChange.when)) { - lastChange.when = branchModel.getDate(); - lastChange.who = branchModel.getAuthorIdent().getName(); - } - } - return lastChange; - } - - // default to the repository folder modification date - return new LastChange(repository.getDirectory().lastModified()); - } - - /** - * Retrieves a Java Date from a Git commit. - * - * @param commit - * @return date of the commit or Date(0) if the commit is null - */ - public static Date getCommitDate(RevCommit commit) { - if (commit == null) { - return new Date(0); - } - return new Date(commit.getCommitTime() * 1000L); - } - - /** - * Retrieves a Java Date from a Git commit. - * - * @param commit - * @return date of the commit or Date(0) if the commit is null - */ - public static Date getAuthorDate(RevCommit commit) { - if (commit == null) { - return new Date(0); - } - if (commit.getAuthorIdent() != null) { - return commit.getAuthorIdent().getWhen(); - } - return getCommitDate(commit); - } - - /** - * Returns the specified commit from the repository. If the repository does - * not exist or is empty, null is returned. - * - * @param repository - * @param objectId - * if unspecified, HEAD is assumed. - * @return RevCommit - */ - public static RevCommit getCommit(Repository repository, String objectId) { - if (!hasCommits(repository)) { - return null; - } - RevCommit commit = null; - RevWalk walk = null; - try { - // resolve object id - ObjectId branchObject; - if (StringUtils.isEmpty(objectId) || "HEAD".equalsIgnoreCase(objectId)) { - branchObject = getDefaultBranch(repository); - } else { - branchObject = repository.resolve(objectId); - } - if (branchObject == null) { - return null; - } - walk = new RevWalk(repository); - RevCommit rev = walk.parseCommit(branchObject); - commit = rev; - } catch (Throwable t) { - error(t, repository, "{0} failed to get commit {1}", objectId); - } finally { - if (walk != null) { - walk.dispose(); - } - } - return commit; - } - - /** - * Retrieves the raw byte content of a file in the specified tree. - * - * @param repository - * @param tree - * if null, the RevTree from HEAD is assumed. - * @param path - * @return content as a byte [] - */ - public static byte[] getByteContent(Repository repository, RevTree tree, final String path, boolean throwError) { - RevWalk rw = new RevWalk(repository); - TreeWalk tw = new TreeWalk(repository); - tw.setFilter(PathFilterGroup.createFromStrings(Collections.singleton(path))); - byte[] content = null; - try { - if (tree == null) { - ObjectId object = getDefaultBranch(repository); - if (object == null) - return null; - RevCommit commit = rw.parseCommit(object); - tree = commit.getTree(); - } - tw.reset(tree); - while (tw.next()) { - if (tw.isSubtree() && !path.equals(tw.getPathString())) { - tw.enterSubtree(); - continue; - } - ObjectId entid = tw.getObjectId(0); - FileMode entmode = tw.getFileMode(0); - if (entmode != FileMode.GITLINK) { - ObjectLoader ldr = repository.open(entid, Constants.OBJ_BLOB); - content = ldr.getCachedBytes(); - } - } - } catch (Throwable t) { - if (throwError) { - error(t, repository, "{0} can't find {1} in tree {2}", path, tree.name()); - } - } finally { - rw.dispose(); - tw.close(); - } - return content; - } - - /** - * Returns the UTF-8 string content of a file in the specified tree. - * - * @param repository - * @param tree - * if null, the RevTree from HEAD is assumed. - * @param blobPath - * @param charsets optional - * @return UTF-8 string content - */ - public static String getStringContent(Repository repository, RevTree tree, String blobPath, String... charsets) { - byte[] content = getByteContent(repository, tree, blobPath, true); - if (content == null) { - return null; - } - return StringUtils.decodeString(content, charsets); - } - - /** - * Gets the raw byte content of the specified blob object. - * - * @param repository - * @param objectId - * @return byte [] blob content - */ - public static byte[] getByteContent(Repository repository, String objectId) { - RevWalk rw = new RevWalk(repository); - byte[] content = null; - try { - RevBlob blob = rw.lookupBlob(ObjectId.fromString(objectId)); - ObjectLoader ldr = repository.open(blob.getId(), Constants.OBJ_BLOB); - content = ldr.getCachedBytes(); - } catch (Throwable t) { - error(t, repository, "{0} can't find blob {1}", objectId); - } finally { - rw.dispose(); - } - return content; - } - - /** - * Gets the UTF-8 string content of the blob specified by objectId. - * - * @param repository - * @param objectId - * @param charsets optional - * @return UTF-8 string content - */ - public static String getStringContent(Repository repository, String objectId, String... charsets) { - byte[] content = getByteContent(repository, objectId); - if (content == null) { - return null; - } - return StringUtils.decodeString(content, charsets); - } - - /** - * Returns the list of files in the specified folder at the specified - * commit. If the repository does not exist or is empty, an empty list is - * returned. - * - * @param repository - * @param path - * if unspecified, root folder is assumed. - * @param commit - * if null, HEAD is assumed. - * @return list of files in specified path - */ - public static List getFilesInPath(Repository repository, String path, - RevCommit commit) { - List list = new ArrayList(); - if (!hasCommits(repository)) { - return list; - } - if (commit == null) { - commit = getCommit(repository, null); - } - final TreeWalk tw = new TreeWalk(repository); - try { - tw.addTree(commit.getTree()); - if (!StringUtils.isEmpty(path)) { - PathFilter f = PathFilter.create(path); - tw.setFilter(f); - tw.setRecursive(false); - boolean foundFolder = false; - while (tw.next()) { - if (!foundFolder && tw.isSubtree()) { - tw.enterSubtree(); - } - if (tw.getPathString().equals(path)) { - foundFolder = true; - continue; - } - if (foundFolder) { - list.add(getPathModel(tw, path, commit)); - } - } - } else { - tw.setRecursive(false); - while (tw.next()) { - list.add(getPathModel(tw, null, commit)); - } - } - } catch (IOException e) { - error(e, repository, "{0} failed to get files for commit {1}", commit.getName()); - } finally { - tw.close(); - } - Collections.sort(list); - return list; - } - - /** - * Returns the list of files in the specified folder at the specified - * commit. If the repository does not exist or is empty, an empty list is - * returned. - * - * This is modified version that implements path compression feature. - * - * @param repository - * @param path - * if unspecified, root folder is assumed. - * @param commit - * if null, HEAD is assumed. - * @return list of files in specified path - */ - public static List getFilesInPath2(Repository repository, String path, RevCommit commit) { - - List list = new ArrayList(); - if (!hasCommits(repository)) { - return list; - } - if (commit == null) { - commit = getCommit(repository, null); - } - final TreeWalk tw = new TreeWalk(repository); - try { - - tw.addTree(commit.getTree()); - final boolean isPathEmpty = Strings.isNullOrEmpty(path); - - if (!isPathEmpty) { - PathFilter f = PathFilter.create(path); - tw.setFilter(f); - } - - tw.setRecursive(true); - List paths = new ArrayList<>(); - - while (tw.next()) { - String pathString = tw.getPathString(); - String child = isPathEmpty ? pathString : pathString.replaceFirst(Pattern.quote(String.format("%s/", path)), ""); - paths.add(child); - } - - for(String p: PathUtils.compressPaths(paths)) { - String pathString = isPathEmpty ? p : String.format("%s/%s", path, p); - list.add(getPathModel(repository, pathString, path, commit)); - } - - } catch (IOException e) { - error(e, repository, "{0} failed to get files for commit {1}", commit.getName()); - } finally { - tw.close(); - } - Collections.sort(list); - return list; - } - - /** - * Returns the list of files changed in a specified commit. If the - * repository does not exist or is empty, an empty list is returned. - * - * @param repository - * @param commit - * if null, HEAD is assumed. - * @return list of files changed in a commit - */ - public static List getFilesInCommit(Repository repository, RevCommit commit) { - return getFilesInCommit(repository, commit, true); - } - - /** - * Returns the list of files changed in a specified commit. If the - * repository does not exist or is empty, an empty list is returned. - * - * @param repository - * @param commit - * if null, HEAD is assumed. - * @param calculateDiffStat - * if true, each PathChangeModel will have insertions/deletions - * @return list of files changed in a commit - */ - public static List getFilesInCommit(Repository repository, RevCommit commit, boolean calculateDiffStat) { - List list = new ArrayList(); - if (!hasCommits(repository)) { - return list; - } - RevWalk rw = new RevWalk(repository); - try { - if (commit == null) { - ObjectId object = getDefaultBranch(repository); - commit = rw.parseCommit(object); - } - - if (commit.getParentCount() == 0) { - TreeWalk tw = new TreeWalk(repository); - tw.reset(); - tw.setRecursive(true); - tw.addTree(commit.getTree()); - while (tw.next()) { - long size = 0; - FilestoreModel filestoreItem = null; - ObjectId objectId = tw.getObjectId(0); - - try { - if (!tw.isSubtree() && (tw.getFileMode(0) != FileMode.GITLINK)) { - - size = tw.getObjectReader().getObjectSize(objectId, Constants.OBJ_BLOB); - - if (isPossibleFilestoreItem(size)) { - filestoreItem = getFilestoreItem(tw.getObjectReader().open(objectId)); - } - } - } catch (Throwable t) { - error(t, null, "failed to retrieve blob size for " + tw.getPathString()); - } - - list.add(new PathChangeModel(tw.getPathString(), tw.getPathString(),filestoreItem, size, tw - .getRawMode(0), objectId.getName(), commit.getId().getName(), - ChangeType.ADD)); - } - tw.close(); - } else { - RevCommit parent = rw.parseCommit(commit.getParent(0).getId()); - DiffStatFormatter df = new DiffStatFormatter(commit.getName(), repository); - df.setRepository(repository); - df.setDiffComparator(RawTextComparator.DEFAULT); - df.setDetectRenames(true); - List diffs = df.scan(parent.getTree(), commit.getTree()); - for (DiffEntry diff : diffs) { - // create the path change model - PathChangeModel pcm = PathChangeModel.from(diff, commit.getName(), repository); - - if (calculateDiffStat) { - // update file diffstats - df.format(diff); - PathChangeModel pathStat = df.getDiffStat().getPath(pcm.path); - if (pathStat != null) { - pcm.insertions = pathStat.insertions; - pcm.deletions = pathStat.deletions; - } - } - list.add(pcm); - } - } - } catch (Throwable t) { - error(t, repository, "{0} failed to determine files in commit!"); - } finally { - rw.dispose(); - } - return list; - } - - /** - * Returns the list of files changed in a specified commit. If the - * repository does not exist or is empty, an empty list is returned. - * - * @param repository - * @param startCommit - * earliest commit - * @param endCommit - * most recent commit. if null, HEAD is assumed. - * @return list of files changed in a commit range - */ - public static List getFilesInRange(Repository repository, String startCommit, String endCommit) { - List list = new ArrayList(); - if (!hasCommits(repository)) { - return list; - } - try { - ObjectId startRange = repository.resolve(startCommit); - ObjectId endRange = repository.resolve(endCommit); - RevWalk rw = new RevWalk(repository); - RevCommit start = rw.parseCommit(startRange); - RevCommit end = rw.parseCommit(endRange); - list.addAll(getFilesInRange(repository, start, end)); - rw.close(); - } catch (Throwable t) { - error(t, repository, "{0} failed to determine files in range {1}..{2}!", startCommit, endCommit); - } - return list; - } - - /** - * Returns the list of files changed in a specified commit. If the - * repository does not exist or is empty, an empty list is returned. - * - * @param repository - * @param startCommit - * earliest commit - * @param endCommit - * most recent commit. if null, HEAD is assumed. - * @return list of files changed in a commit range - */ - public static List getFilesInRange(Repository repository, RevCommit startCommit, RevCommit endCommit) { - List list = new ArrayList(); - if (!hasCommits(repository)) { - return list; - } - try { - DiffFormatter df = new DiffFormatter(null); - df.setRepository(repository); - df.setDiffComparator(RawTextComparator.DEFAULT); - df.setDetectRenames(true); - - List diffEntries = df.scan(startCommit.getTree(), endCommit.getTree()); - for (DiffEntry diff : diffEntries) { - PathChangeModel pcm = PathChangeModel.from(diff, endCommit.getName(), repository); - list.add(pcm); - } - Collections.sort(list); - } catch (Throwable t) { - error(t, repository, "{0} failed to determine files in range {1}..{2}!", startCommit, endCommit); - } - return list; - } - /** - * Returns the list of files in the repository on the default branch that - * match one of the specified extensions. This is a CASE-SENSITIVE search. - * If the repository does not exist or is empty, an empty list is returned. - * - * @param repository - * @param extensions - * @return list of files in repository with a matching extension - */ - public static List getDocuments(Repository repository, List extensions) { - return getDocuments(repository, extensions, null); - } - - /** - * Returns the list of files in the repository in the specified commit that - * match one of the specified extensions. This is a CASE-SENSITIVE search. - * If the repository does not exist or is empty, an empty list is returned. - * - * @param repository - * @param extensions - * @param objectId - * @return list of files in repository with a matching extension - */ - public static List getDocuments(Repository repository, List extensions, - String objectId) { - List list = new ArrayList(); - if (!hasCommits(repository)) { - return list; - } - RevCommit commit = getCommit(repository, objectId); - final TreeWalk tw = new TreeWalk(repository); - try { - tw.addTree(commit.getTree()); - if (extensions != null && extensions.size() > 0) { - List suffixFilters = new ArrayList(); - for (String extension : extensions) { - if (extension.charAt(0) == '.') { - suffixFilters.add(PathSuffixFilter.create(extension)); - } else { - // escape the . since this is a regexp filter - suffixFilters.add(PathSuffixFilter.create("." + extension)); - } - } - TreeFilter filter; - if (suffixFilters.size() == 1) { - filter = suffixFilters.get(0); - } else { - filter = OrTreeFilter.create(suffixFilters); - } - tw.setFilter(filter); - tw.setRecursive(true); - } - while (tw.next()) { - list.add(getPathModel(tw, null, commit)); - } - } catch (IOException e) { - error(e, repository, "{0} failed to get documents for commit {1}", commit.getName()); - } finally { - tw.close(); - } - Collections.sort(list); - return list; - } - - /** - * Returns a path model of the current file in the treewalk. - * - * @param tw - * @param basePath - * @param commit - * @return a path model of the current file in the treewalk - */ - private static PathModel getPathModel(TreeWalk tw, String basePath, RevCommit commit) { - String name; - long size = 0; - - if (StringUtils.isEmpty(basePath)) { - name = tw.getPathString(); - } else { - name = tw.getPathString().substring(basePath.length() + 1); - } - ObjectId objectId = tw.getObjectId(0); - FilestoreModel filestoreItem = null; - - try { - if (!tw.isSubtree() && (tw.getFileMode(0) != FileMode.GITLINK)) { - - size = tw.getObjectReader().getObjectSize(objectId, Constants.OBJ_BLOB); - - if (isPossibleFilestoreItem(size)) { - filestoreItem = getFilestoreItem(tw.getObjectReader().open(objectId)); - } - } - } catch (Throwable t) { - error(t, null, "failed to retrieve blob size for " + tw.getPathString()); - } - return new PathModel(name, tw.getPathString(), filestoreItem, size, tw.getFileMode(0).getBits(), - objectId.getName(), commit.getName()); - } - - public static boolean isPossibleFilestoreItem(long size) { - return ( (size >= com.gitblit.Constants.LEN_FILESTORE_META_MIN) - && (size <= com.gitblit.Constants.LEN_FILESTORE_META_MAX)); - } - - /** - * - * @return Representative FilestoreModel if valid, otherwise null - */ - public static FilestoreModel getFilestoreItem(ObjectLoader obj){ - try { - final byte[] blob = obj.getCachedBytes(com.gitblit.Constants.LEN_FILESTORE_META_MAX); - final String meta = new String(blob, "UTF-8"); - - return FilestoreModel.fromMetaString(meta); - - } catch (LargeObjectException e) { - //Intentionally failing silent - } catch (Exception e) { - error(e, null, "failed to retrieve filestoreItem " + obj.toString()); - } - - return null; - } - - /** - * Returns a path model by path string - * - * @param repo - * @param path - * @param filter - * @param commit - * @return a path model of the specified object - */ - private static PathModel getPathModel(Repository repo, String path, String filter, RevCommit commit) - throws IOException { - - long size = 0; - FilestoreModel filestoreItem = null; - TreeWalk tw = TreeWalk.forPath(repo, path, commit.getTree()); - String pathString = path; - - if (!tw.isSubtree() && (tw.getFileMode(0) != FileMode.GITLINK)) { - - pathString = PathUtils.getLastPathComponent(pathString); - - size = tw.getObjectReader().getObjectSize(tw.getObjectId(0), Constants.OBJ_BLOB); - - if (isPossibleFilestoreItem(size)) { - filestoreItem = getFilestoreItem(tw.getObjectReader().open(tw.getObjectId(0))); - } - } else if (tw.isSubtree()) { - - // do not display dirs that are behind in the path - if (!Strings.isNullOrEmpty(filter)) { - pathString = path.replaceFirst(filter + "/", ""); - } - - // remove the last slash from path in displayed link - if (pathString != null && pathString.charAt(pathString.length()-1) == '/') { - pathString = pathString.substring(0, pathString.length()-1); - } - } - - return new PathModel(pathString, tw.getPathString(), filestoreItem, size, tw.getFileMode(0).getBits(), - tw.getObjectId(0).getName(), commit.getName()); - - } - - - /** - * Returns a permissions representation of the mode bits. - * - * @param mode - * @return string representation of the mode bits - */ - public static String getPermissionsFromMode(int mode) { - if (FileMode.TREE.equals(mode)) { - return "drwxr-xr-x"; - } else if (FileMode.REGULAR_FILE.equals(mode)) { - return "-rw-r--r--"; - } else if (FileMode.EXECUTABLE_FILE.equals(mode)) { - return "-rwxr-xr-x"; - } else if (FileMode.SYMLINK.equals(mode)) { - return "symlink"; - } else if (FileMode.GITLINK.equals(mode)) { - return "submodule"; - } - return "missing"; - } - - /** - * Returns a list of commits since the minimum date starting from the - * specified object id. - * - * @param repository - * @param objectId - * if unspecified, HEAD is assumed. - * @param minimumDate - * @return list of commits - */ - public static List getRevLog(Repository repository, String objectId, Date minimumDate) { - List list = new ArrayList(); - if (!hasCommits(repository)) { - return list; - } - try { - // resolve branch - ObjectId branchObject; - if (StringUtils.isEmpty(objectId)) { - branchObject = getDefaultBranch(repository); - } else { - branchObject = repository.resolve(objectId); - } - - RevWalk rw = new RevWalk(repository); - rw.markStart(rw.parseCommit(branchObject)); - rw.setRevFilter(CommitTimeRevFilter.after(minimumDate)); - Iterable revlog = rw; - for (RevCommit rev : revlog) { - list.add(rev); - } - rw.dispose(); - } catch (Throwable t) { - error(t, repository, "{0} failed to get {1} revlog for minimum date {2}", objectId, - minimumDate); - } - return list; - } - - /** - * Returns a list of commits starting from HEAD and working backwards. - * - * @param repository - * @param maxCount - * if < 0, all commits for the repository are returned. - * @return list of commits - */ - public static List getRevLog(Repository repository, int maxCount) { - return getRevLog(repository, null, 0, maxCount); - } - - /** - * Returns a list of commits starting from the specified objectId using an - * offset and maxCount for paging. This is similar to LIMIT n OFFSET p in - * SQL. If the repository does not exist or is empty, an empty list is - * returned. - * - * @param repository - * @param objectId - * if unspecified, HEAD is assumed. - * @param offset - * @param maxCount - * if < 0, all commits are returned. - * @return a paged list of commits - */ - public static List getRevLog(Repository repository, String objectId, int offset, - int maxCount) { - return getRevLog(repository, objectId, null, offset, maxCount); - } - - /** - * Returns a list of commits for the repository or a path within the - * repository. Caller may specify ending revision with objectId. Caller may - * specify offset and maxCount to achieve pagination of results. If the - * repository does not exist or is empty, an empty list is returned. - * - * @param repository - * @param objectId - * if unspecified, HEAD is assumed. - * @param path - * if unspecified, commits for repository are returned. If - * specified, commits for the path are returned. - * @param offset - * @param maxCount - * if < 0, all commits are returned. - * @return a paged list of commits - */ - public static List getRevLog(Repository repository, String objectId, String path, - int offset, int maxCount) { - List list = new ArrayList(); - if (maxCount == 0) { - return list; - } - if (!hasCommits(repository)) { - return list; - } - try { - // resolve branch - ObjectId startRange = null; - ObjectId endRange; - if (StringUtils.isEmpty(objectId)) { - endRange = getDefaultBranch(repository); - } else { - if( objectId.contains("..") ) { - // range expression - String[] parts = objectId.split("\\.\\."); - startRange = repository.resolve(parts[0]); - endRange = repository.resolve(parts[1]); - } else { - // objectid - endRange= repository.resolve(objectId); - } - } - if (endRange == null) { - return list; - } - - RevWalk rw = new RevWalk(repository); - rw.markStart(rw.parseCommit(endRange)); - if (startRange != null) { - rw.markUninteresting(rw.parseCommit(startRange)); - } - if (!StringUtils.isEmpty(path)) { - TreeFilter filter = AndTreeFilter.create( - PathFilterGroup.createFromStrings(Collections.singleton(path)), - TreeFilter.ANY_DIFF); - rw.setTreeFilter(filter); - } - Iterable revlog = rw; - if (offset > 0) { - int count = 0; - for (RevCommit rev : revlog) { - count++; - if (count > offset) { - list.add(rev); - if (maxCount > 0 && list.size() == maxCount) { - break; - } - } - } - } else { - for (RevCommit rev : revlog) { - list.add(rev); - if (maxCount > 0 && list.size() == maxCount) { - break; - } - } - } - rw.dispose(); - } catch (Throwable t) { - error(t, repository, "{0} failed to get {1} revlog for path {2}", objectId, path); - } - return list; - } - - /** - * Returns a list of commits for the repository within the range specified - * by startRangeId and endRangeId. If the repository does not exist or is - * empty, an empty list is returned. - * - * @param repository - * @param startRangeId - * the first commit (not included in results) - * @param endRangeId - * the end commit (included in results) - * @return a list of commits - */ - public static List getRevLog(Repository repository, String startRangeId, - String endRangeId) { - List list = new ArrayList(); - if (!hasCommits(repository)) { - return list; - } - try { - ObjectId endRange = repository.resolve(endRangeId); - ObjectId startRange = repository.resolve(startRangeId); - - RevWalk rw = new RevWalk(repository); - rw.markStart(rw.parseCommit(endRange)); - if (startRange.equals(ObjectId.zeroId())) { - // maybe this is a tag or an orphan branch - list.add(rw.parseCommit(endRange)); - rw.dispose(); - return list; - } else { - rw.markUninteresting(rw.parseCommit(startRange)); - } - - Iterable revlog = rw; - for (RevCommit rev : revlog) { - list.add(rev); - } - rw.dispose(); - } catch (Throwable t) { - error(t, repository, "{0} failed to get revlog for {1}..{2}", startRangeId, endRangeId); - } - return list; - } - - /** - * Search the commit history for a case-insensitive match to the value. - * Search results require a specified SearchType of AUTHOR, COMMITTER, or - * COMMIT. Results may be paginated using offset and maxCount. If the - * repository does not exist or is empty, an empty list is returned. - * - * @param repository - * @param objectId - * if unspecified, HEAD is assumed. - * @param value - * @param type - * AUTHOR, COMMITTER, COMMIT - * @param offset - * @param maxCount - * if < 0, all matches are returned - * @return matching list of commits - */ - public static List searchRevlogs(Repository repository, String objectId, - String value, final com.gitblit.Constants.SearchType type, int offset, int maxCount) { - List list = new ArrayList(); - if (StringUtils.isEmpty(value)) { - return list; - } - if (maxCount == 0) { - return list; - } - if (!hasCommits(repository)) { - return list; - } - final String lcValue = value.toLowerCase(); - try { - // resolve branch - ObjectId branchObject; - if (StringUtils.isEmpty(objectId)) { - branchObject = getDefaultBranch(repository); - } else { - branchObject = repository.resolve(objectId); - } - - RevWalk rw = new RevWalk(repository); - rw.setRevFilter(new RevFilter() { - - @Override - public RevFilter clone() { - // FindBugs complains about this method name. - // This is part of JGit design and unrelated to Cloneable. - return this; - } - - @Override - public boolean include(RevWalk walker, RevCommit commit) throws StopWalkException, - MissingObjectException, IncorrectObjectTypeException, IOException { - boolean include = false; - switch (type) { - case AUTHOR: - include = (commit.getAuthorIdent().getName().toLowerCase().indexOf(lcValue) > -1) - || (commit.getAuthorIdent().getEmailAddress().toLowerCase() - .indexOf(lcValue) > -1); - break; - case COMMITTER: - include = (commit.getCommitterIdent().getName().toLowerCase() - .indexOf(lcValue) > -1) - || (commit.getCommitterIdent().getEmailAddress().toLowerCase() - .indexOf(lcValue) > -1); - break; - case COMMIT: - include = commit.getFullMessage().toLowerCase().indexOf(lcValue) > -1; - break; - } - return include; - } - - }); - rw.markStart(rw.parseCommit(branchObject)); - Iterable revlog = rw; - if (offset > 0) { - int count = 0; - for (RevCommit rev : revlog) { - count++; - if (count > offset) { - list.add(rev); - if (maxCount > 0 && list.size() == maxCount) { - break; - } - } - } - } else { - for (RevCommit rev : revlog) { - list.add(rev); - if (maxCount > 0 && list.size() == maxCount) { - break; - } - } - } - rw.dispose(); - } catch (Throwable t) { - error(t, repository, "{0} failed to {1} search revlogs for {2}", type.name(), value); - } - return list; - } - - /** - * Returns the default branch to use for a repository. Normally returns - * whatever branch HEAD points to, but if HEAD points to nothing it returns - * the most recently updated branch. - * - * @param repository - * @return the objectid of a branch - * @throws Exception - */ - public static ObjectId getDefaultBranch(Repository repository) throws Exception { - ObjectId object = repository.resolve(Constants.HEAD); - if (object == null) { - // no HEAD - // perhaps non-standard repository, try local branches - List branchModels = getLocalBranches(repository, true, -1); - if (branchModels.size() > 0) { - // use most recently updated branch - RefModel branch = null; - Date lastDate = new Date(0); - for (RefModel branchModel : branchModels) { - if (branchModel.getDate().after(lastDate)) { - branch = branchModel; - lastDate = branch.getDate(); - } - } - object = branch.getReferencedObjectId(); - } - } - return object; - } - - /** - * Returns the target of the symbolic HEAD reference for a repository. - * Normally returns a branch reference name, but when HEAD is detached, - * the commit is matched against the known tags. The most recent matching - * tag ref name will be returned if it references the HEAD commit. If - * no match is found, the SHA1 is returned. - * - * @param repository - * @return the ref name or the SHA1 for a detached HEAD - */ - public static String getHEADRef(Repository repository) { - String target = null; - try { - target = repository.getFullBranch(); - } catch (Throwable t) { - error(t, repository, "{0} failed to get symbolic HEAD target"); - } - return target; - } - - /** - * Sets the symbolic ref HEAD to the specified target ref. The - * HEAD will be detached if the target ref is not a branch. - * - * @param repository - * @param targetRef - * @return true if successful - */ - public static boolean setHEADtoRef(Repository repository, String targetRef) { - try { - // detach HEAD if target ref is not a branch - boolean detach = !targetRef.startsWith(Constants.R_HEADS); - RefUpdate.Result result; - RefUpdate head = repository.updateRef(Constants.HEAD, detach); - if (detach) { // Tag - RevCommit commit = getCommit(repository, targetRef); - head.setNewObjectId(commit.getId()); - result = head.forceUpdate(); - } else { - result = head.link(targetRef); - } - switch (result) { - case NEW: - case FORCED: - case NO_CHANGE: - case FAST_FORWARD: - return true; - default: - LOGGER.error(MessageFormat.format("{0} HEAD update to {1} returned result {2}", - repository.getDirectory().getAbsolutePath(), targetRef, result)); - } - } catch (Throwable t) { - error(t, repository, "{0} failed to set HEAD to {1}", targetRef); - } - return false; - } - - /** - * Sets the local branch ref to point to the specified commit id. - * - * @param repository - * @param branch - * @param commitId - * @return true if successful - */ - public static boolean setBranchRef(Repository repository, String branch, String commitId) { - String branchName = branch; - if (!branchName.startsWith(Constants.R_REFS)) { - branchName = Constants.R_HEADS + branch; - } - - try { - RefUpdate refUpdate = repository.updateRef(branchName, false); - refUpdate.setNewObjectId(ObjectId.fromString(commitId)); - RefUpdate.Result result = refUpdate.forceUpdate(); - - switch (result) { - case NEW: - case FORCED: - case NO_CHANGE: - case FAST_FORWARD: - return true; - default: - LOGGER.error(MessageFormat.format("{0} {1} update to {2} returned result {3}", - repository.getDirectory().getAbsolutePath(), branchName, commitId, result)); - } - } catch (Throwable t) { - error(t, repository, "{0} failed to set {1} to {2}", branchName, commitId); - } - return false; - } - - /** - * Deletes the specified branch ref. - * - * @param repository - * @param branch - * @return true if successful - */ - public static boolean deleteBranchRef(Repository repository, String branch) { - - try { - RefUpdate refUpdate = repository.updateRef(branch, false); - refUpdate.setForceUpdate(true); - RefUpdate.Result result = refUpdate.delete(); - switch (result) { - case NEW: - case FORCED: - case NO_CHANGE: - case FAST_FORWARD: - return true; - default: - LOGGER.error(MessageFormat.format("{0} failed to delete to {1} returned result {2}", - repository.getDirectory().getAbsolutePath(), branch, result)); - } - } catch (Throwable t) { - error(t, repository, "{0} failed to delete {1}", branch); - } - return false; - } - - /** - * Get the full branch and tag ref names for any potential HEAD targets. - * - * @param repository - * @return a list of ref names - */ - public static List getAvailableHeadTargets(Repository repository) { - List targets = new ArrayList(); - for (RefModel branchModel : JGitUtils.getLocalBranches(repository, true, -1)) { - targets.add(branchModel.getName()); - } - - for (RefModel tagModel : JGitUtils.getTags(repository, true, -1)) { - targets.add(tagModel.getName()); - } - return targets; - } - - /** - * Returns all refs grouped by their associated object id. - * - * @param repository - * @return all refs grouped by their referenced object id - */ - public static Map> getAllRefs(Repository repository) { - return getAllRefs(repository, true); - } - - /** - * Returns all refs grouped by their associated object id. - * - * @param repository - * @param includeRemoteRefs - * @return all refs grouped by their referenced object id - */ - public static Map> getAllRefs(Repository repository, boolean includeRemoteRefs) { - List list = getRefs(repository, org.eclipse.jgit.lib.RefDatabase.ALL, true, -1); - Map> refs = new HashMap>(); - for (RefModel ref : list) { - if (!includeRemoteRefs && ref.getName().startsWith(Constants.R_REMOTES)) { - continue; - } - ObjectId objectid = ref.getReferencedObjectId(); - if (!refs.containsKey(objectid)) { - refs.put(objectid, new ArrayList()); - } - refs.get(objectid).add(ref); - } - return refs; - } - - /** - * Returns the list of tags in the repository. If repository does not exist - * or is empty, an empty list is returned. - * - * @param repository - * @param fullName - * if true, /refs/tags/yadayadayada is returned. If false, - * yadayadayada is returned. - * @param maxCount - * if < 0, all tags are returned - * @return list of tags - */ - public static List getTags(Repository repository, boolean fullName, int maxCount) { - return getRefs(repository, Constants.R_TAGS, fullName, maxCount); - } - - /** - * Returns the list of tags in the repository. If repository does not exist - * or is empty, an empty list is returned. - * - * @param repository - * @param fullName - * if true, /refs/tags/yadayadayada is returned. If false, - * yadayadayada is returned. - * @param maxCount - * if < 0, all tags are returned - * @param offset - * if maxCount provided sets the starting point of the records to return - * @return list of tags - */ - public static List getTags(Repository repository, boolean fullName, int maxCount, int offset) { - return getRefs(repository, Constants.R_TAGS, fullName, maxCount, offset); - } - - /** - * Returns the list of local branches in the repository. If repository does - * not exist or is empty, an empty list is returned. - * - * @param repository - * @param fullName - * if true, /refs/heads/yadayadayada is returned. If false, - * yadayadayada is returned. - * @param maxCount - * if < 0, all local branches are returned - * @return list of local branches - */ - public static List getLocalBranches(Repository repository, boolean fullName, - int maxCount) { - return getRefs(repository, Constants.R_HEADS, fullName, maxCount); - } - - /** - * Returns the list of remote branches in the repository. If repository does - * not exist or is empty, an empty list is returned. - * - * @param repository - * @param fullName - * if true, /refs/remotes/yadayadayada is returned. If false, - * yadayadayada is returned. - * @param maxCount - * if < 0, all remote branches are returned - * @return list of remote branches - */ - public static List getRemoteBranches(Repository repository, boolean fullName, - int maxCount) { - return getRefs(repository, Constants.R_REMOTES, fullName, maxCount); - } - - /** - * Returns the list of note branches. If repository does not exist or is - * empty, an empty list is returned. - * - * @param repository - * @param fullName - * if true, /refs/notes/yadayadayada is returned. If false, - * yadayadayada is returned. - * @param maxCount - * if < 0, all note branches are returned - * @return list of note branches - */ - public static List getNoteBranches(Repository repository, boolean fullName, - int maxCount) { - return getRefs(repository, Constants.R_NOTES, fullName, maxCount); - } - - /** - * Returns the list of refs in the specified base ref. If repository does - * not exist or is empty, an empty list is returned. - * - * @param repository - * @param fullName - * if true, /refs/yadayadayada is returned. If false, - * yadayadayada is returned. - * @return list of refs - */ - public static List getRefs(Repository repository, String baseRef) { - return getRefs(repository, baseRef, true, -1); - } - - /** - * Returns a list of references in the repository matching "refs". If the - * repository is null or empty, an empty list is returned. - * - * @param repository - * @param refs - * if unspecified, all refs are returned - * @param fullName - * if true, /refs/something/yadayadayada is returned. If false, - * yadayadayada is returned. - * @param maxCount - * if < 0, all references are returned - * @return list of references - */ - private static List getRefs(Repository repository, String refs, boolean fullName, - int maxCount) { - return getRefs(repository, refs, fullName, maxCount, 0); - } - - /** - * Returns a list of references in the repository matching "refs". If the - * repository is null or empty, an empty list is returned. - * - * @param repository - * @param refs - * if unspecified, all refs are returned - * @param fullName - * if true, /refs/something/yadayadayada is returned. If false, - * yadayadayada is returned. - * @param maxCount - * if < 0, all references are returned - * @param offset - * if maxCount provided sets the starting point of the records to return - * @return list of references - */ - private static List getRefs(Repository repository, String refs, boolean fullName, - int maxCount, int offset) { - List list = new ArrayList(); - if (maxCount == 0) { - return list; - } - if (!hasCommits(repository)) { - return list; - } - try { - Map map = repository.getRefDatabase().getRefs(refs); - RevWalk rw = new RevWalk(repository); - for (Entry entry : map.entrySet()) { - Ref ref = entry.getValue(); - RevObject object = rw.parseAny(ref.getObjectId()); - String name = entry.getKey(); - if (fullName && !StringUtils.isEmpty(refs)) { - name = refs + name; - } - list.add(new RefModel(name, ref, object)); - } - rw.dispose(); - Collections.sort(list); - Collections.reverse(list); - if (maxCount > 0 && list.size() > maxCount) { - if (offset < 0) { - offset = 0; - } - int endIndex = offset + maxCount; - if (endIndex > list.size()) { - endIndex = list.size(); - } - list = new ArrayList(list.subList(offset, endIndex)); - } - } catch (IOException e) { - error(e, repository, "{0} failed to retrieve {1}", refs); - } - return list; - } - - /** - * Returns a RefModel for the gh-pages branch in the repository. If the - * branch can not be found, null is returned. - * - * @param repository - * @return a refmodel for the gh-pages branch or null - */ - public static RefModel getPagesBranch(Repository repository) { - return getBranch(repository, "gh-pages"); - } - - /** - * Returns a RefModel for a specific branch name in the repository. If the - * branch can not be found, null is returned. - * - * @param repository - * @return a refmodel for the branch or null - */ - public static RefModel getBranch(Repository repository, String name) { - RefModel branch = null; - try { - // search for the branch in local heads - for (RefModel ref : JGitUtils.getLocalBranches(repository, false, -1)) { - if (ref.reference.getName().endsWith(name)) { - branch = ref; - break; - } - } - - // search for the branch in remote heads - if (branch == null) { - for (RefModel ref : JGitUtils.getRemoteBranches(repository, false, -1)) { - if (ref.reference.getName().endsWith(name)) { - branch = ref; - break; - } - } - } - } catch (Throwable t) { - LOGGER.error(MessageFormat.format("Failed to find {0} branch!", name), t); - } - return branch; - } - - /** - * Returns the list of submodules for this repository. - * - * @param repository - * @param commit - * @return list of submodules - */ - public static List getSubmodules(Repository repository, String commitId) { - RevCommit commit = getCommit(repository, commitId); - return getSubmodules(repository, commit.getTree()); - } - - /** - * Returns the list of submodules for this repository. - * - * @param repository - * @param commit - * @return list of submodules - */ - public static List getSubmodules(Repository repository, RevTree tree) { - List list = new ArrayList(); - byte [] blob = getByteContent(repository, tree, ".gitmodules", false); - if (blob == null) { - return list; - } - try { - BlobBasedConfig config = new BlobBasedConfig(repository.getConfig(), blob); - for (String module : config.getSubsections("submodule")) { - String path = config.getString("submodule", module, "path"); - String url = config.getString("submodule", module, "url"); - list.add(new SubmoduleModel(module, path, url)); - } - } catch (ConfigInvalidException e) { - LOGGER.error("Failed to load .gitmodules file for " + repository.getDirectory(), e); - } - return list; - } - - /** - * Returns the submodule definition for the specified path at the specified - * commit. If no module is defined for the path, null is returned. - * - * @param repository - * @param commit - * @param path - * @return a submodule definition or null if there is no submodule - */ - public static SubmoduleModel getSubmoduleModel(Repository repository, String commitId, String path) { - for (SubmoduleModel model : getSubmodules(repository, commitId)) { - if (model.path.equals(path)) { - return model; - } - } - return null; - } - - public static String getSubmoduleCommitId(Repository repository, String path, RevCommit commit) { - String commitId = null; - RevWalk rw = new RevWalk(repository); - TreeWalk tw = new TreeWalk(repository); - tw.setFilter(PathFilterGroup.createFromStrings(Collections.singleton(path))); - try { - tw.reset(commit.getTree()); - while (tw.next()) { - if (tw.isSubtree() && !path.equals(tw.getPathString())) { - tw.enterSubtree(); - continue; - } - if (FileMode.GITLINK == tw.getFileMode(0)) { - commitId = tw.getObjectId(0).getName(); - break; - } - } - } catch (Throwable t) { - error(t, repository, "{0} can't find {1} in commit {2}", path, commit.name()); - } finally { - rw.dispose(); - tw.close(); - } - return commitId; - } - - /** - * Returns the list of notes entered about the commit from the refs/notes - * namespace. If the repository does not exist or is empty, an empty list is - * returned. - * - * @param repository - * @param commit - * @return list of notes - */ - public static List getNotesOnCommit(Repository repository, RevCommit commit) { - List list = new ArrayList(); - if (!hasCommits(repository)) { - return list; - } - List noteBranches = getNoteBranches(repository, true, -1); - for (RefModel notesRef : noteBranches) { - RevTree notesTree = JGitUtils.getCommit(repository, notesRef.getName()).getTree(); - // flat notes list - String notePath = commit.getName(); - String text = getStringContent(repository, notesTree, notePath); - if (!StringUtils.isEmpty(text)) { - List history = getRevLog(repository, notesRef.getName(), notePath, 0, -1); - RefModel noteRef = new RefModel(notesRef.displayName, null, history.get(history - .size() - 1)); - GitNote gitNote = new GitNote(noteRef, text); - list.add(gitNote); - continue; - } - - // folder structure - StringBuilder sb = new StringBuilder(commit.getName()); - sb.insert(2, '/'); - notePath = sb.toString(); - text = getStringContent(repository, notesTree, notePath); - if (!StringUtils.isEmpty(text)) { - List history = getRevLog(repository, notesRef.getName(), notePath, 0, -1); - RefModel noteRef = new RefModel(notesRef.displayName, null, history.get(history - .size() - 1)); - GitNote gitNote = new GitNote(noteRef, text); - list.add(gitNote); - } - } - return list; - } - - /** - * this method creates an incremental revision number as a tag according to - * the amount of already existing tags, which start with a defined prefix. - * - * @param repository - * @param objectId - * @param tagger - * @param prefix - * @param intPattern - * @param message - * @return true if operation was successful, otherwise false - */ - public static boolean createIncrementalRevisionTag(Repository repository, - String objectId, PersonIdent tagger, String prefix, String intPattern, String message) { - boolean result = false; - Iterator> iterator = repository.getTags().entrySet().iterator(); - long lastRev = 0; - while (iterator.hasNext()) { - Entry entry = iterator.next(); - if (entry.getKey().startsWith(prefix)) { - try { - long val = Long.parseLong(entry.getKey().substring(prefix.length())); - if (val > lastRev) { - lastRev = val; - } - } catch (Exception e) { - // this tag is NOT an incremental revision tag - } - } - } - DecimalFormat df = new DecimalFormat(intPattern); - result = createTag(repository, objectId, tagger, prefix + df.format((lastRev + 1)), message); - return result; - } - - /** - * creates a tag in a repository - * - * @param repository - * @param objectId, the ref the tag points towards - * @param tagger, the person tagging the object - * @param tag, the string label - * @param message, the string message - * @return boolean, true if operation was successful, otherwise false - */ - public static boolean createTag(Repository repository, String objectId, PersonIdent tagger, String tag, String message) { - try { - Git gitClient = Git.open(repository.getDirectory()); - TagCommand tagCommand = gitClient.tag(); - tagCommand.setTagger(tagger); - tagCommand.setMessage(message); - if (objectId != null) { - RevObject revObj = getCommit(repository, objectId); - tagCommand.setObjectId(revObj); - } - tagCommand.setName(tag); - Ref call = tagCommand.call(); - return call != null ? true : false; - } catch (Exception e) { - error(e, repository, "Failed to create tag {1} in repository {0}", objectId, tag); - } - return false; - } - - /** - * Create an orphaned branch in a repository. - * - * @param repository - * @param branchName - * @param author - * if unspecified, Gitblit will be the author of this new branch - * @return true if successful - */ - public static boolean createOrphanBranch(Repository repository, String branchName, - PersonIdent author) { - boolean success = false; - String message = "Created branch " + branchName; - if (author == null) { - author = new PersonIdent("Gitblit", "gitblit@localhost"); - } - try { - ObjectInserter odi = repository.newObjectInserter(); - try { - // Create a blob object to insert into a tree - ObjectId blobId = odi.insert(Constants.OBJ_BLOB, - message.getBytes(Constants.CHARACTER_ENCODING)); - - // Create a tree object to reference from a commit - TreeFormatter tree = new TreeFormatter(); - tree.append(".branch", FileMode.REGULAR_FILE, blobId); - ObjectId treeId = odi.insert(tree); - - // Create a commit object - CommitBuilder commit = new CommitBuilder(); - commit.setAuthor(author); - commit.setCommitter(author); - commit.setEncoding(Constants.CHARACTER_ENCODING); - commit.setMessage(message); - commit.setTreeId(treeId); - - // Insert the commit into the repository - ObjectId commitId = odi.insert(commit); - odi.flush(); - - RevWalk revWalk = new RevWalk(repository); - try { - RevCommit revCommit = revWalk.parseCommit(commitId); - if (!branchName.startsWith("refs/")) { - branchName = "refs/heads/" + branchName; - } - RefUpdate ru = repository.updateRef(branchName); - ru.setNewObjectId(commitId); - ru.setRefLogMessage("commit: " + revCommit.getShortMessage(), false); - Result rc = ru.forceUpdate(); - switch (rc) { - case NEW: - case FORCED: - case FAST_FORWARD: - success = true; - break; - default: - success = false; - } - } finally { - revWalk.close(); - } - } finally { - odi.close(); - } - } catch (Throwable t) { - error(t, repository, "Failed to create orphan branch {1} in repository {0}", branchName); - } - return success; - } - - /** - * Reads the sparkleshare id, if present, from the repository. - * - * @param repository - * @return an id or null - */ - public static String getSparkleshareId(Repository repository) { - byte[] content = getByteContent(repository, null, ".sparkleshare", false); - if (content == null) { - return null; - } - return StringUtils.decodeString(content); - } - - /** - * Automatic repair of (some) invalid refspecs. These are the result of a - * bug in JGit cloning where a double forward-slash was injected. :( - * - * @param repository - * @return true, if the refspecs were repaired - */ - public static boolean repairFetchSpecs(Repository repository) { - StoredConfig rc = repository.getConfig(); - - // auto-repair broken fetch ref specs - for (String name : rc.getSubsections("remote")) { - int invalidSpecs = 0; - int repairedSpecs = 0; - List specs = new ArrayList(); - for (String spec : rc.getStringList("remote", name, "fetch")) { - try { - RefSpec rs = new RefSpec(spec); - // valid spec - specs.add(spec); - } catch (IllegalArgumentException e) { - // invalid spec - invalidSpecs++; - if (spec.contains("//")) { - // auto-repair this known spec bug - spec = spec.replace("//", "/"); - specs.add(spec); - repairedSpecs++; - } - } - } - - if (invalidSpecs == repairedSpecs && repairedSpecs > 0) { - // the fetch specs were automatically repaired - rc.setStringList("remote", name, "fetch", specs); - try { - rc.save(); - rc.load(); - LOGGER.debug("repaired {} invalid fetch refspecs for {}", repairedSpecs, repository.getDirectory()); - return true; - } catch (Exception e) { - LOGGER.error(null, e); - } - } else if (invalidSpecs > 0) { - LOGGER.error("mirror executor found {} invalid fetch refspecs for {}", invalidSpecs, repository.getDirectory()); - } - } - return false; - } - - /** - * Returns true if the commit identified by commitId is an ancestor or the - * the commit identified by tipId. - * - * @param repository - * @param commitId - * @param tipId - * @return true if there is the commit is an ancestor of the tip - */ - public static boolean isMergedInto(Repository repository, String commitId, String tipId) { - try { - return isMergedInto(repository, repository.resolve(commitId), repository.resolve(tipId)); - } catch (Exception e) { - LOGGER.error("Failed to determine isMergedInto", e); - } - return false; - } - - /** - * Returns true if the commit identified by commitId is an ancestor or the - * the commit identified by tipId. - * - * @param repository - * @param commitId - * @param tipId - * @return true if there is the commit is an ancestor of the tip - */ - public static boolean isMergedInto(Repository repository, ObjectId commitId, ObjectId tipCommitId) { - // traverse the revlog looking for a commit chain between the endpoints - RevWalk rw = new RevWalk(repository); - try { - // must re-lookup RevCommits to workaround undocumented RevWalk bug - RevCommit tip = rw.lookupCommit(tipCommitId); - RevCommit commit = rw.lookupCommit(commitId); - return rw.isMergedInto(commit, tip); - } catch (Exception e) { - LOGGER.error("Failed to determine isMergedInto", e); - } finally { - rw.dispose(); - } - return false; - } - - /** - * Returns the merge base of two commits or null if there is no common - * ancestry. - * - * @param repository - * @param commitIdA - * @param commitIdB - * @return the commit id of the merge base or null if there is no common base - */ - public static String getMergeBase(Repository repository, ObjectId commitIdA, ObjectId commitIdB) { - RevWalk rw = new RevWalk(repository); - try { - RevCommit a = rw.lookupCommit(commitIdA); - RevCommit b = rw.lookupCommit(commitIdB); - - rw.setRevFilter(RevFilter.MERGE_BASE); - rw.markStart(a); - rw.markStart(b); - RevCommit mergeBase = rw.next(); - if (mergeBase == null) { - return null; - } - return mergeBase.getName(); - } catch (Exception e) { - LOGGER.error("Failed to determine merge base", e); - } finally { - rw.dispose(); - } - return null; - } - - public static enum MergeStatus { - MISSING_INTEGRATION_BRANCH, MISSING_SRC_BRANCH, NOT_MERGEABLE, FAILED, ALREADY_MERGED, MERGEABLE, MERGED; - } - - /** - * Determines if we can cleanly merge one branch into another. Returns true - * if we can merge without conflict, otherwise returns false. - * - * @param repository - * @param src - * @param toBranch - * @param mergeType - * Defines the integration strategy to use for merging. - * @return true if we can merge without conflict - */ - public static MergeStatus canMerge(Repository repository, String src, String toBranch, MergeType mergeType) { - IntegrationStrategy strategy = IntegrationStrategyFactory.create(mergeType, repository, src, toBranch); - return strategy.canMerge(); - } - - - public static class MergeResult { - public final MergeStatus status; - public final String sha; - - MergeResult(MergeStatus status, String sha) { - this.status = status; - this.sha = sha; - } - } - - /** - * Tries to merge a commit into a branch. If there are conflicts, the merge - * will fail. - * - * @param repository - * @param src - * @param toBranch - * @param mergeType - * Defines the integration strategy to use for merging. - * @param committer - * @param message - * @return the merge result - */ - public static MergeResult merge(Repository repository, String src, String toBranch, MergeType mergeType, - PersonIdent committer, String message) { - - if (!toBranch.startsWith(Constants.R_REFS)) { - // branch ref doesn't start with ref, assume this is a branch head - toBranch = Constants.R_HEADS + toBranch; - } - - IntegrationStrategy strategy = IntegrationStrategyFactory.create(mergeType, repository, src, toBranch); - MergeResult mergeResult = strategy.merge(committer, message); - - if (mergeResult.status != MergeStatus.MERGED) { - return mergeResult; - } - - try { - // Update the integration branch ref - RefUpdate mergeRefUpdate = repository.updateRef(toBranch); - mergeRefUpdate.setNewObjectId(strategy.getMergeCommit()); - mergeRefUpdate.setRefLogMessage(strategy.getRefLogMessage(), false); - mergeRefUpdate.setExpectedOldObjectId(strategy.branchTip); - RefUpdate.Result rc = mergeRefUpdate.update(); - switch (rc) { - case FAST_FORWARD: - // successful, clean merge - break; - default: - mergeResult = new MergeResult(MergeStatus.FAILED, null); - throw new GitBlitException(MessageFormat.format("Unexpected result \"{0}\" when {1} in {2}", - rc.name(), strategy.getOperationMessage(), repository.getDirectory())); - } - } catch (IOException e) { - LOGGER.error("Failed to merge", e); - } - - return mergeResult; - } - - - private static abstract class IntegrationStrategy { - Repository repository; - String src; - String toBranch; - - RevWalk revWalk; - RevCommit branchTip; - RevCommit srcTip; - - RevCommit mergeCommit; - String refLogMessage; - String operationMessage; - - RevCommit getMergeCommit() { - return mergeCommit; - } - - String getRefLogMessage() { - return refLogMessage; - } - - String getOperationMessage() { - return operationMessage; - } - - IntegrationStrategy(Repository repository, String src, String toBranch) { - this.repository = repository; - this.src = src; - this.toBranch = toBranch; - } - - void prepare() throws IOException { - if (revWalk == null) revWalk = new RevWalk(repository); - ObjectId branchId = repository.resolve(toBranch); - if (branchId != null) { - branchTip = revWalk.lookupCommit(branchId); - } - ObjectId srcId = repository.resolve(src); - if (srcId != null) { - srcTip = revWalk.lookupCommit(srcId); - } - } - - - abstract MergeStatus _canMerge() throws IOException; - - - MergeStatus canMerge() { - try { - prepare(); - if (branchTip == null) { - return MergeStatus.MISSING_INTEGRATION_BRANCH; - } - if (srcTip == null) { - return MergeStatus.MISSING_SRC_BRANCH; - } - if (revWalk.isMergedInto(srcTip, branchTip)) { - // already merged - return MergeStatus.ALREADY_MERGED; - } - // determined by specific integration strategy - return _canMerge(); - - } catch (NullPointerException e) { - LOGGER.error("Failed to determine canMerge", e); - } catch (IOException e) { - LOGGER.error("Failed to determine canMerge", e); - } finally { - if (revWalk != null) { - revWalk.close(); - } - } - - return MergeStatus.NOT_MERGEABLE; - } - - - abstract MergeResult _merge(PersonIdent committer, String message) throws IOException; - - - MergeResult merge(PersonIdent committer, String message) { - try { - prepare(); - if (revWalk.isMergedInto(srcTip, branchTip)) { - // already merged - return new MergeResult(MergeStatus.ALREADY_MERGED, null); - } - // determined by specific integration strategy - return _merge(committer, message); - - } catch (IOException e) { - LOGGER.error("Failed to merge", e); - } finally { - if (revWalk != null) { - revWalk.close(); - } - } - - return new MergeResult(MergeStatus.FAILED, null); - } - } - - - private static class FastForwardOnly extends IntegrationStrategy { - FastForwardOnly(Repository repository, String src, String toBranch) { - super(repository, src, toBranch); - } - - @Override - MergeStatus _canMerge() throws IOException { - if (revWalk.isMergedInto(branchTip, srcTip)) { - // fast-forward - return MergeStatus.MERGEABLE; - } - - return MergeStatus.NOT_MERGEABLE; - } - - @Override - MergeResult _merge(PersonIdent committer, String message) throws IOException { - if (! revWalk.isMergedInto(branchTip, srcTip)) { - // is not fast-forward - return new MergeResult(MergeStatus.FAILED, null); - } - - mergeCommit = srcTip; - refLogMessage = "merge " + src + ": Fast-forward"; - operationMessage = MessageFormat.format("fast-forwarding {0} to commit {1}", srcTip.getName(), branchTip.getName()); - - return new MergeResult(MergeStatus.MERGED, srcTip.getName()); - } - } - - private static class MergeIfNecessary extends IntegrationStrategy { - MergeIfNecessary(Repository repository, String src, String toBranch) { - super(repository, src, toBranch); - } - - @Override - MergeStatus _canMerge() throws IOException { - if (revWalk.isMergedInto(branchTip, srcTip)) { - // fast-forward - return MergeStatus.MERGEABLE; - } - - RecursiveMerger merger = (RecursiveMerger) MergeStrategy.RECURSIVE.newMerger(repository, true); - boolean canMerge = merger.merge(branchTip, srcTip); - if (canMerge) { - return MergeStatus.MERGEABLE; - } - - return MergeStatus.NOT_MERGEABLE; - } - - @Override - MergeResult _merge(PersonIdent committer, String message) throws IOException { - if (revWalk.isMergedInto(branchTip, srcTip)) { - // fast-forward - mergeCommit = srcTip; - refLogMessage = "merge " + src + ": Fast-forward"; - operationMessage = MessageFormat.format("fast-forwarding {0} to commit {1}", branchTip.getName(), srcTip.getName()); - - return new MergeResult(MergeStatus.MERGED, srcTip.getName()); - } - - RecursiveMerger merger = (RecursiveMerger) MergeStrategy.RECURSIVE.newMerger(repository, true); - boolean merged = merger.merge(branchTip, srcTip); - if (merged) { - // create a merge commit and a reference to track the merge commit - ObjectId treeId = merger.getResultTreeId(); - ObjectInserter odi = repository.newObjectInserter(); - try { - // Create a commit object - CommitBuilder commitBuilder = new CommitBuilder(); - commitBuilder.setCommitter(committer); - commitBuilder.setAuthor(committer); - commitBuilder.setEncoding(Constants.CHARSET); - if (StringUtils.isEmpty(message)) { - message = MessageFormat.format("merge {0} into {1}", srcTip.getName(), branchTip.getName()); - } - commitBuilder.setMessage(message); - commitBuilder.setParentIds(branchTip.getId(), srcTip.getId()); - commitBuilder.setTreeId(treeId); - - // Insert the merge commit into the repository - ObjectId mergeCommitId = odi.insert(commitBuilder); - odi.flush(); - - mergeCommit = revWalk.parseCommit(mergeCommitId); - refLogMessage = "commit: " + mergeCommit.getShortMessage(); - operationMessage = MessageFormat.format("merging commit {0} into {1}", srcTip.getName(), branchTip.getName()); - - // return the merge commit id - return new MergeResult(MergeStatus.MERGED, mergeCommitId.getName()); - } finally { - odi.close(); - } - } - return new MergeResult(MergeStatus.FAILED, null); - } - } - - private static class MergeAlways extends IntegrationStrategy { - MergeAlways(Repository repository, String src, String toBranch) { - super(repository, src, toBranch); - } - - @Override - MergeStatus _canMerge() throws IOException { - RecursiveMerger merger = (RecursiveMerger) MergeStrategy.RECURSIVE.newMerger(repository, true); - boolean canMerge = merger.merge(branchTip, srcTip); - if (canMerge) { - return MergeStatus.MERGEABLE; - } - - return MergeStatus.NOT_MERGEABLE; - } - - @Override - MergeResult _merge(PersonIdent committer, String message) throws IOException { - RecursiveMerger merger = (RecursiveMerger) MergeStrategy.RECURSIVE.newMerger(repository, true); - boolean merged = merger.merge(branchTip, srcTip); - if (merged) { - // create a merge commit and a reference to track the merge commit - ObjectId treeId = merger.getResultTreeId(); - ObjectInserter odi = repository.newObjectInserter(); - try { - // Create a commit object - CommitBuilder commitBuilder = new CommitBuilder(); - commitBuilder.setCommitter(committer); - commitBuilder.setAuthor(committer); - commitBuilder.setEncoding(Constants.CHARSET); - if (StringUtils.isEmpty(message)) { - message = MessageFormat.format("merge {0} into {1}", srcTip.getName(), branchTip.getName()); - } - commitBuilder.setMessage(message); - commitBuilder.setParentIds(branchTip.getId(), srcTip.getId()); - commitBuilder.setTreeId(treeId); - - // Insert the merge commit into the repository - ObjectId mergeCommitId = odi.insert(commitBuilder); - odi.flush(); - - mergeCommit = revWalk.parseCommit(mergeCommitId); - refLogMessage = "commit: " + mergeCommit.getShortMessage(); - operationMessage = MessageFormat.format("merging commit {0} into {1}", srcTip.getName(), branchTip.getName()); - - // return the merge commit id - return new MergeResult(MergeStatus.MERGED, mergeCommitId.getName()); - } finally { - odi.close(); - } - } - - return new MergeResult(MergeStatus.FAILED, null); - } - } - - - private static class IntegrationStrategyFactory { - static IntegrationStrategy create(MergeType mergeType, Repository repository, String src, String toBranch) { - switch(mergeType) { - case FAST_FORWARD_ONLY: - return new FastForwardOnly(repository, src, toBranch); - case MERGE_IF_NECESSARY: - return new MergeIfNecessary(repository, src, toBranch); - case MERGE_ALWAYS: - return new MergeAlways(repository, src, toBranch); - } - return null; - } - } - - - /** - * Returns the LFS URL for the given oid - * Currently assumes that the Gitblit Filestore is used - * - * @param baseURL - * @param repository name - * @param oid of lfs item - * @return the lfs item URL - */ - public static String getLfsRepositoryUrl(String baseURL, String repositoryName, String oid) { - - if (baseURL.length() > 0 && baseURL.charAt(baseURL.length() - 1) == '/') { - baseURL = baseURL.substring(0, baseURL.length() - 1); - } - - return baseURL + com.gitblit.Constants.R_PATH - + repositoryName + "/" - + com.gitblit.Constants.R_LFS - + "objects/" + oid; - - } - - /** - * Returns all tree entries that do not match the ignore paths. - * - * @param db - * @param ignorePaths - * @param dcBuilder - * @throws IOException - */ - public static List getTreeEntries(Repository db, String branch, Collection ignorePaths) throws IOException { - List list = new ArrayList(); - TreeWalk tw = null; - try { - ObjectId treeId = db.resolve(branch + "^{tree}"); - if (treeId == null) { - // branch does not exist yet - return list; - } - tw = new TreeWalk(db); - int hIdx = tw.addTree(treeId); - tw.setRecursive(true); - - while (tw.next()) { - String path = tw.getPathString(); - CanonicalTreeParser hTree = null; - if (hIdx != -1) { - hTree = tw.getTree(hIdx, CanonicalTreeParser.class); - } - if (!ignorePaths.contains(path)) { - // add all other tree entries - if (hTree != null) { - final DirCacheEntry entry = new DirCacheEntry(path); - entry.setObjectId(hTree.getEntryObjectId()); - entry.setFileMode(hTree.getEntryFileMode()); - list.add(entry); - } - } - } - } finally { - if (tw != null) { - tw.close(); - } - } - return list; - } - - public static boolean commitIndex(Repository db, String branch, DirCache index, - ObjectId parentId, boolean forceCommit, - String author, String authorEmail, String message) throws IOException, ConcurrentRefUpdateException { - boolean success = false; - - ObjectId headId = db.resolve(branch + "^{commit}"); - ObjectId baseId = parentId; - if (baseId == null || headId == null) { return false; } - - ObjectInserter odi = db.newObjectInserter(); - try { - // Create the in-memory index of the new/updated ticket - ObjectId indexTreeId = index.writeTree(odi); - - // Create a commit object - PersonIdent ident = new PersonIdent(author, authorEmail); - - if (forceCommit == false) { - ThreeWayMerger merger = MergeStrategy.RECURSIVE.newMerger(db, true); - merger.setObjectInserter(odi); - merger.setBase(baseId); - boolean mergeSuccess = merger.merge(indexTreeId, headId); - - if (mergeSuccess) { - indexTreeId = merger.getResultTreeId(); - } else { - //Manual merge required - return false; - } - } - - CommitBuilder commit = new CommitBuilder(); - commit.setAuthor(ident); - commit.setCommitter(ident); - commit.setEncoding(com.gitblit.Constants.ENCODING); - commit.setMessage(message); - commit.setParentId(headId); - commit.setTreeId(indexTreeId); - - // Insert the commit into the repository - ObjectId commitId = odi.insert(commit); - odi.flush(); - - RevWalk revWalk = new RevWalk(db); - try { - RevCommit revCommit = revWalk.parseCommit(commitId); - RefUpdate ru = db.updateRef(branch); - ru.setForceUpdate(forceCommit); - ru.setNewObjectId(commitId); - ru.setExpectedOldObjectId(headId); - ru.setRefLogMessage("commit: " + revCommit.getShortMessage(), false); - Result rc = ru.update(); - - switch (rc) { - case NEW: - case FORCED: - case FAST_FORWARD: - success = true; - break; - case REJECTED: - case LOCK_FAILURE: - throw new ConcurrentRefUpdateException(JGitText.get().couldNotLockHEAD, - ru.getRef(), rc); - default: - throw new JGitInternalException(MessageFormat.format( - JGitText.get().updatingRefFailed, branch, commitId.toString(), - rc)); - } - } finally { - revWalk.close(); - } - } finally { - odi.close(); - } - return success; - } - - /** - * Returns true if the commit identified by commitId is at the tip of it's branch. - * - * @param repository - * @param commitId - * @return true if the given commit is the tip - */ - public static boolean isTip(Repository repository, String commitId) { - try { - RefModel tip = getBranch(repository, commitId); - return (tip != null); - } catch (Exception e) { - LOGGER.error("Failed to determine isTip", e); - } - return false; - } - - /* - * Identify ticket by considering the branch the commit is on - * - * @param repository - * @param commit - * @return ticket number, or 0 if no ticket - */ - public static long getTicketNumberFromCommitBranch(Repository repository, RevCommit commit) { - // try lookup by change ref - Map> map = repository.getAllRefsByPeeledObjectId(); - Set refs = map.get(commit.getId()); - if (!ArrayUtils.isEmpty(refs)) { - for (Ref ref : refs) { - long number = PatchsetCommand.getTicketNumber(ref.getName()); - - if (number > 0) { - return number; - } - } - } - - return 0; - } - - - /** - * Try to identify all referenced tickets from the commit. - * - * @param commit - * @return a collection of TicketLinks - */ - @NotNull - public static List identifyTicketsFromCommitMessage(Repository repository, IStoredSettings settings, - RevCommit commit) { - List ticketLinks = new ArrayList(); - List linkedTickets = new ArrayList(); - - // parse commit message looking for fixes/closes #n - final String xFixDefault = "(?:fixes|closes)[\\s-]+#?(\\d+)"; - String xFix = settings.getString(Keys.tickets.closeOnPushCommitMessageRegex, xFixDefault); - if (StringUtils.isEmpty(xFix)) { - xFix = xFixDefault; - } - try { - Pattern p = Pattern.compile(xFix, Pattern.CASE_INSENSITIVE); - Matcher m = p.matcher(commit.getFullMessage()); - while (m.find()) { - String val = m.group(1); - long number = Long.parseLong(val); - - if (number > 0) { - ticketLinks.add(new TicketLink(number, TicketAction.Close)); - linkedTickets.add(number); - } - } - } catch (Exception e) { - LOGGER.error(String.format("Failed to parse \"%s\" in commit %s", xFix, commit.getName()), e); - } - - // parse commit message looking for ref #n - final String xRefDefault = "(?:ref|task|issue|bug)?[\\s-]*#(\\d+)"; - String xRef = settings.getString(Keys.tickets.linkOnPushCommitMessageRegex, xRefDefault); - if (StringUtils.isEmpty(xRef)) { - xRef = xRefDefault; - } - try { - Pattern p = Pattern.compile(xRef, Pattern.CASE_INSENSITIVE); - Matcher m = p.matcher(commit.getFullMessage()); - while (m.find()) { - String val = m.group(1); - long number = Long.parseLong(val); - //Most generic case so don't included tickets more precisely linked - if ((number > 0) && (!linkedTickets.contains(number))) { - ticketLinks.add( new TicketLink(number, TicketAction.Commit, commit.getName())); - linkedTickets.add(number); - } - } - } catch (Exception e) { - LOGGER.error(String.format("Failed to parse \"%s\" in commit %s", xRef, commit.getName()), e); - } - - return ticketLinks; - } - - /** - * Try to identify all referenced tickets between two commits - * - * @param commit - * @param parseMessage - * @param currentTicketId, or 0 if not on a ticket branch - * @return a collection of TicketLink, or null if commit is already linked - */ - public static List identifyTicketsBetweenCommits(Repository repository, IStoredSettings settings, - String baseSha, String tipSha) { - List links = new ArrayList(); - if (repository == null) { return links; } - - RevWalk walk = new RevWalk(repository); - walk.sort(RevSort.TOPO); - walk.sort(RevSort.REVERSE, true); - try { - RevCommit tip = walk.parseCommit(repository.resolve(tipSha)); - RevCommit base = walk.parseCommit(repository.resolve(baseSha)); - walk.markStart(tip); - walk.markUninteresting(base); - for (;;) { - RevCommit commit = walk.next(); - if (commit == null) { - break; - } - links.addAll(JGitUtils.identifyTicketsFromCommitMessage(repository, settings, commit)); - } - } catch (IOException e) { - LOGGER.error("failed to identify tickets between commits.", e); - } finally { - walk.dispose(); - } - - return links; - } - - public static int countCommits(Repository repository, RevWalk walk, ObjectId baseId, ObjectId tipId) { - int count = 0; - walk.reset(); - walk.sort(RevSort.TOPO); - walk.sort(RevSort.REVERSE, true); - try { - RevCommit tip = walk.parseCommit(tipId); - RevCommit base = walk.parseCommit(baseId); - walk.markStart(tip); - walk.markUninteresting(base); - for (;;) { - RevCommit c = walk.next(); - if (c == null) { - break; - } - count++; - } - } catch (IOException e) { - // Should never happen, the core receive process would have - // identified the missing object earlier before we got control. - LOGGER.error("failed to get commit count", e); - return 0; - } finally { - walk.close(); - } - return count; - } - - public static int countCommits(Repository repository, RevWalk walk, String baseId, String tipId) { - int count = 0; - try { - count = countCommits(repository, walk, repository.resolve(baseId),repository.resolve(tipId)); - } catch (IOException e) { - LOGGER.error("failed to get commit count", e); - } - return count; - } -} From d70c8a92becee00c8269a494f5c0a3a390e169ae Mon Sep 17 00:00:00 2001 From: Tomasz Sztejka Date: Thu, 4 Nov 2021 11:48:54 +0100 Subject: [PATCH 15/21] Made sure that web app in deployment mode do use default resource value if resource is missing. This may result in strange texts to be shown to user if resource is missing, "[Warning: Property for 'xxxx' not found]" but it is better than Internal Server Error which do totally disable functionality. --- src/main/java/com/gitblit/wicket/GitBlitWebApp.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/main/java/com/gitblit/wicket/GitBlitWebApp.java b/src/main/java/com/gitblit/wicket/GitBlitWebApp.java index c10d88732..c42a2318c 100644 --- a/src/main/java/com/gitblit/wicket/GitBlitWebApp.java +++ b/src/main/java/com/gitblit/wicket/GitBlitWebApp.java @@ -190,8 +190,10 @@ public void init() { } // configure the resource cache duration to 90 days for deployment + // and enable resources to fall back to defaults if not found if (!isDebugMode()) { getResourceSettings().setDefaultCacheDuration(90 * 86400); + getResourceSettings().setUseDefaultOnMissingResource(true); } // setup the standard gitweb-ish urls From 3301feaf8782a34dd1684cd266da36164a30d205 Mon Sep 17 00:00:00 2001 From: Tomasz Sztejka Date: Thu, 4 Nov 2021 12:35:51 +0100 Subject: [PATCH 16/21] Basically a question expained in commit. Use of AtomicInteger as "mutable interger" is an overkill and performance loss. --- .../gitblit/wicket/panels/CommitLegendPanel.java | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/gitblit/wicket/panels/CommitLegendPanel.java b/src/main/java/com/gitblit/wicket/panels/CommitLegendPanel.java index 3f31effe4..c69f05cd0 100644 --- a/src/main/java/com/gitblit/wicket/panels/CommitLegendPanel.java +++ b/src/main/java/com/gitblit/wicket/panels/CommitLegendPanel.java @@ -29,13 +29,20 @@ import org.apache.wicket.markup.repeater.data.ListDataProvider; import org.eclipse.jgit.diff.DiffEntry.ChangeType; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + import com.gitblit.models.PathModel.PathChangeModel; import com.gitblit.wicket.WicketUtils; public class CommitLegendPanel extends Panel { private static final long serialVersionUID = 1L; - + static final Logger LOGGER = LoggerFactory.getLogger(CommitLegendPanel.class); + static final boolean TRACE = LOGGER.isTraceEnabled(); + static final boolean DEBUG = LOGGER.isDebugEnabled(); + public CommitLegendPanel(String id, List paths) { super(id); final Map stats = getChangedPathsStats(paths); @@ -75,7 +82,9 @@ public void populateItem(final Item item) { }; add(legendsView); } - + //Question: why AtomicInterger? Wicket is NOT using threads, especially not in that place. + //Is only reason of Atomic here is that it is mutable? If yes, it is a pure loss of resources + //since each atomic access do case a hell of cache flushes. protected Map getChangedPathsStats(List paths) { Map stats = new HashMap(); for (PathChangeModel path : paths) { From db81e86a830555062cea877fff8634d527cf8dd9 Mon Sep 17 00:00:00 2001 From: Tomasz Sztejka Date: Thu, 4 Nov 2021 13:50:18 +0100 Subject: [PATCH 17/21] Gosh.... I know why now. All due to DataView API which needs wrappers over wrappers over.... where a simple array would be sufficient. Now I start to understand what kind of thinking is behind high memory consumption and low overall performance. It wasn't like that when I was a lad and had 960 bytes of RAM on board... ;) --- .../java/com/gitblit/wicket/panels/CommitLegendPanel.java | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/main/java/com/gitblit/wicket/panels/CommitLegendPanel.java b/src/main/java/com/gitblit/wicket/panels/CommitLegendPanel.java index c69f05cd0..2b7fa91f5 100644 --- a/src/main/java/com/gitblit/wicket/panels/CommitLegendPanel.java +++ b/src/main/java/com/gitblit/wicket/panels/CommitLegendPanel.java @@ -43,6 +43,7 @@ public class CommitLegendPanel extends Panel { static final boolean TRACE = LOGGER.isTraceEnabled(); static final boolean DEBUG = LOGGER.isDebugEnabled(); + public CommitLegendPanel(String id, List paths) { super(id); final Map stats = getChangedPathsStats(paths); @@ -82,9 +83,7 @@ public void populateItem(final Item item) { }; add(legendsView); } - //Question: why AtomicInterger? Wicket is NOT using threads, especially not in that place. - //Is only reason of Atomic here is that it is mutable? If yes, it is a pure loss of resources - //since each atomic access do case a hell of cache flushes. + protected Map getChangedPathsStats(List paths) { Map stats = new HashMap(); for (PathChangeModel path : paths) { From 91898b8918ed39d9d01dafb15fb146ca821b049f Mon Sep 17 00:00:00 2001 From: Tomasz Sztejka Date: Thu, 4 Nov 2021 15:38:50 +0100 Subject: [PATCH 18/21] 1.Added some logging code 2.Added property which controls limit of files to process to show diff stats in commit. 3.Updated code responsible for computing diffs to respect it. 4.Ensured that server will not report Internal Error if missing resource string is found. 5.Added necessary resource strings in EN and PL translations. Noticed that there is no script which transforms UTF-8 props to ASCII what is a pain for translators, but ignore id. 6.Updated some comments regarding loggers and their retrival startegy 7.Updated CommitLegendPanel and DiffStatsPanel to inform user that there are some more changes if limit set in 2. was tripped. 8.Code is ready for clean-up and full testing. --- src/main/distrib/data/defaults.properties | 9 ++++ src/main/java/com/gitblit/GitBlitServer.java | 5 +- .../java/com/gitblit/utils/JGitUtils.java | 46 +++++++++++++++++-- .../com/gitblit/wicket/GitBlitWebApp.java | 1 + .../gitblit/wicket/GitBlitWebApp.properties | 4 +- .../wicket/GitBlitWebApp_pl.properties | 2 + .../com/gitblit/wicket/pages/BasePage.java | 15 +++--- .../com/gitblit/wicket/pages/CommitPage.java | 28 +++++++++-- .../wicket/panels/CommitLegendPanel.html | 3 +- .../wicket/panels/CommitLegendPanel.java | 17 +++++-- .../gitblit/wicket/panels/DiffStatPanel.java | 38 ++++++++++++--- 11 files changed, 140 insertions(+), 28 deletions(-) diff --git a/src/main/distrib/data/defaults.properties b/src/main/distrib/data/defaults.properties index b5787e1e3..d11377bbd 100644 --- a/src/main/distrib/data/defaults.properties +++ b/src/main/distrib/data/defaults.properties @@ -1552,6 +1552,15 @@ web.maxDiffLinesPerFile = 4000 # SINCE 1.7.0 web.maxDiffLines = 20000 +# Maximum number of changed files in single commit to process and display +# on commit page. Set to -1 to disable limit. +# +# Keeping this limit at -1 may cause lags and memory overloads if huge +# commits with many files are present in system. +# +# SINCE 1.9.2 +web.maxCommitPaths=-1 + # Enable/disable global regex substitutions (i.e. shared across repositories) # # SINCE 0.5.0 diff --git a/src/main/java/com/gitblit/GitBlitServer.java b/src/main/java/com/gitblit/GitBlitServer.java index a0bd9523e..80fd6cca5 100644 --- a/src/main/java/com/gitblit/GitBlitServer.java +++ b/src/main/java/com/gitblit/GitBlitServer.java @@ -203,10 +203,7 @@ public static void stop(Params params) { */ protected final void start(Params params, FileSettings settings) { final File baseFolder = getBaseFolder(params); - - /* -------- set up logging ---------*/ - setUpLogging(settings,params); - + logger = LoggerFactory.getLogger(GitBlitServer.class); logger.info("\n" + Constants.getASCIIArt()); diff --git a/src/main/java/com/gitblit/utils/JGitUtils.java b/src/main/java/com/gitblit/utils/JGitUtils.java index e70b4f996..3e80ef58d 100644 --- a/src/main/java/com/gitblit/utils/JGitUtils.java +++ b/src/main/java/com/gitblit/utils/JGitUtils.java @@ -123,7 +123,8 @@ public class JGitUtils { static final Logger LOGGER = LoggerFactory.getLogger(JGitUtils.class); - + static final boolean TRACE = LOGGER.isTraceEnabled(); + static final boolean DEBUG = LOGGER.isDebugEnabled(); /** * Log an error message and exception. * @@ -997,9 +998,29 @@ public static List getFilesInCommit(Repository repository, RevC * if true, each PathChangeModel will have insertions/deletions * @return list of files changed in a commit */ - public static List getFilesInCommit(Repository repository, RevCommit commit, boolean calculateDiffStat) { + public static List getFilesInCommit(Repository repository, RevCommit commit, boolean calculateDiffStat) + { + return getFilesInCommit(repository,commit,calculateDiffStat,-1); + }; + /** + * Returns the list of files changed in a specified commit. If the + * repository does not exist or is empty, an empty list is returned. + * + * @param repository + * @param commit + * if null, HEAD is assumed. + * @param calculateDiffStat + * if true, each PathChangeModel will have insertions/deletions + @param files_limit maximum number of files to process. If there are more files + in commit remaning files are ignored. -1 to disable this limit + * @return list of files changed in a commit. This list will be up to files_limit in size. + */ + public static List getFilesInCommit(Repository repository, RevCommit commit, boolean calculateDiffStat, int files_limit) { + if (TRACE) LOGGER.trace("computing changed files in commit "+commit+" repository "+repository+" with calculateDiffStat="+calculateDiffStat+" files_limit="+files_limit); + assert((files_limit>=0)||(files_limit==-1)); List list = new ArrayList(); if (!hasCommits(repository)) { + if (TRACE) LOGGER.trace("No commits."); return list; } RevWalk rw = new RevWalk(repository); @@ -1010,6 +1031,8 @@ public static List getFilesInCommit(Repository repository, RevC } if (commit.getParentCount() == 0) { + if (TRACE) LOGGER.trace("Commit has no parent, faking diffs."); + int counted_files = 0; TreeWalk tw = new TreeWalk(repository); tw.reset(); tw.setRecursive(true); @@ -1035,15 +1058,24 @@ public static List getFilesInCommit(Repository repository, RevC list.add(new PathChangeModel(tw.getPathString(), tw.getPathString(),filestoreItem, size, tw .getRawMode(0), objectId.getName(), commit.getId().getName(), ChangeType.ADD)); + counted_files++; + if (TRACE) LOGGER.trace("Processed "+counted_files+" commit files"); + if ((files_limit>=0) && (counted_files>=files_limit)) + { + if (TRACE) LOGGER.trace("too many files, stopping computations"); + break; + }; } tw.close(); } else { + if (TRACE) LOGGER.trace("Commit has parent some parent, computing diffs"); RevCommit parent = rw.parseCommit(commit.getParent(0).getId()); DiffStatFormatter df = new DiffStatFormatter(commit.getName(), repository); df.setRepository(repository); df.setDiffComparator(RawTextComparator.DEFAULT); df.setDetectRenames(true); List diffs = df.scan(parent.getTree(), commit.getTree()); + int counted_files=0; for (DiffEntry diff : diffs) { // create the path change model PathChangeModel pcm = PathChangeModel.from(diff, commit.getName(), repository); @@ -1058,6 +1090,13 @@ public static List getFilesInCommit(Repository repository, RevC } } list.add(pcm); + counted_files++; + if (TRACE) LOGGER.trace("Processed "+counted_files+" commit files"); + if ((files_limit>=0) && (counted_files>=files_limit)) + { + if (TRACE) LOGGER.trace("too many files, stopping computations"); + break; + }; } } } catch (Throwable t) { @@ -1065,6 +1104,7 @@ public static List getFilesInCommit(Repository repository, RevC } finally { rw.dispose(); } + if (TRACE) LOGGER.trace("Finished computing commit data"); return list; } @@ -2357,7 +2397,7 @@ public static boolean repairFetchSpecs(Repository repository) { try { rc.save(); rc.load(); - LOGGER.debug("repaired {} invalid fetch refspecs for {}", repairedSpecs, repository.getDirectory()); + if (DEBUG) LOGGER.debug("repaired {} invalid fetch refspecs for {}", repairedSpecs, repository.getDirectory()); return true; } catch (Exception e) { LOGGER.error(null, e); diff --git a/src/main/java/com/gitblit/wicket/GitBlitWebApp.java b/src/main/java/com/gitblit/wicket/GitBlitWebApp.java index c42a2318c..b306fefc2 100644 --- a/src/main/java/com/gitblit/wicket/GitBlitWebApp.java +++ b/src/main/java/com/gitblit/wicket/GitBlitWebApp.java @@ -194,6 +194,7 @@ public void init() { if (!isDebugMode()) { getResourceSettings().setDefaultCacheDuration(90 * 86400); getResourceSettings().setUseDefaultOnMissingResource(true); + getResourceSettings().setThrowExceptionOnMissingResource(false); } // setup the standard gitweb-ish urls diff --git a/src/main/java/com/gitblit/wicket/GitBlitWebApp.properties b/src/main/java/com/gitblit/wicket/GitBlitWebApp.properties index 9c643d926..662e789a7 100644 --- a/src/main/java/com/gitblit/wicket/GitBlitWebApp.properties +++ b/src/main/java/com/gitblit/wicket/GitBlitWebApp.properties @@ -64,6 +64,7 @@ gb.filesModified = {0} files modified gb.filesDeleted = {0} files deleted gb.filesCopied = {0} files copied gb.filesRenamed = {0} files renamed +gb.CommitLegendPanel.moreChanges= + more changes gb.missingUsername = Missing Username gb.edit = edit gb.searchTypeTooltip = Select Search Type @@ -504,7 +505,8 @@ gb.todaysActivityNone = today / none gb.noActivityToday = there has been no activity today gb.anonymousUser= anonymous gb.commitMessageRenderer = commit message renderer -gb.diffStat = {0} insertions & {1} deletions +gb.diffStat = {0} insertions & {1} deletions {2,choice,0#|1#or more} +gb.diffStat.total={0}{1,choice,0#|1# or more} gb.home = home gb.isMirror = this repository is a mirror gb.mirrorOf = mirror of {0} diff --git a/src/main/java/com/gitblit/wicket/GitBlitWebApp_pl.properties b/src/main/java/com/gitblit/wicket/GitBlitWebApp_pl.properties index a4753e72f..40aab96ae 100644 --- a/src/main/java/com/gitblit/wicket/GitBlitWebApp_pl.properties +++ b/src/main/java/com/gitblit/wicket/GitBlitWebApp_pl.properties @@ -502,6 +502,8 @@ gb.todaysActivityStats = dzisiaj / {1} zmian przez {2} autor\u00F3w gb.todaysActivityNone = dzisiaj / brak gb.noActivityToday = brak aktywno\u015Bci w dniu dzisiejszym gb.anonymousUser= anonimowy +gb.diffStat = {0} wstawie\u0144 & {1} usuni\u0119\u0107 {2,choice,0#|1#lub wi\u0119cej} +gb.diffStat.total={0}{1,choice,0#|1# lub wi\u0119cej} # This last property for unit tests to test successful loading of the resource file gb.loadLang = polszczyzna diff --git a/src/main/java/com/gitblit/wicket/pages/BasePage.java b/src/main/java/com/gitblit/wicket/pages/BasePage.java index 85d552290..0a7258ee3 100644 --- a/src/main/java/com/gitblit/wicket/pages/BasePage.java +++ b/src/main/java/com/gitblit/wicket/pages/BasePage.java @@ -76,12 +76,15 @@ public abstract class BasePage extends SessionPage { /** Lazy initialized with {@link #logger} */ - private transient Logger logger; //Note: this class could be static final which would be - //possibly faster and better, but might get into - //initialization ordering problem since GitBlitServer - //do re-configure logs in constructor, rather than - //in static constructor. The entire logger facility could - //be the faster, since JIT could optimize out const calls. + private transient Logger logger; //Note: this field could be static final which would be + //possibly faster and better, but in such case loggers + //would be bound with BasePage.class and there would + //be no possibility to filter log data on per-class. + //It might be better (faster) but some code: + // private static final Logger logger = LoggerFactory.getLogger(x.class) + // private static final boolean TRACE = logger.isTraceEnabled(); + //should be copied into each class. I think, it would be better, but it is + //a matter of personal choice. /** Keeps TRACE logging status. Set up in {@link #getLogger} first call. Zero means "unknown", "1" disabled and "2" enabled */ private transient byte is_Trace_Enabled; diff --git a/src/main/java/com/gitblit/wicket/pages/CommitPage.java b/src/main/java/com/gitblit/wicket/pages/CommitPage.java index 09455ae29..2a214a233 100644 --- a/src/main/java/com/gitblit/wicket/pages/CommitPage.java +++ b/src/main/java/com/gitblit/wicket/pages/CommitPage.java @@ -36,6 +36,7 @@ import org.eclipse.jgit.lib.Repository; import org.eclipse.jgit.revwalk.RevCommit; +import com.gitblit.Keys; import com.gitblit.Constants; import com.gitblit.models.GitNote; import com.gitblit.models.PathModel.PathChangeModel; @@ -139,6 +140,7 @@ public void populateItem(final Item item) { @Override public void populateItem(final Item item) { + if (isTraceEnabled()) logger().trace("Polulating note item "+item.getModelObject()); GitNote entry = item.getModelObject(); item.add(new RefsPanel("refName", repositoryName, Arrays.asList(entry.notesRef))); item.add(createPersonPanel("authorName", entry.notesRef.getAuthorIdent(), @@ -154,8 +156,25 @@ public void populateItem(final Item item) { // changed paths list if (isTraceEnabled()) logger().trace("Loading changed paths"); - List paths = JGitUtils.getFilesInCommit(r, c); - if (isTraceEnabled()) logger().trace("Finished loading changed paths"); + + final List paths; + final boolean commit_is_trimmed; + { + //pick up processing limit from server settings + int paths_limit = app().settings().getInteger(Keys.web.maxCommitPaths,-1); + if (!((paths_limit>=0)||(paths_limit==-1))) + { + logger().warn("web.maxCommitPaths is set to "+paths_limit+" what is not -1 or any positive number. Using -1 instead"); + paths_limit=-1; + }; + //compute with diffs. + paths = JGitUtils.getFilesInCommit(r, c, true,paths_limit); + //check if limit is touched. + //Yes, i know, there is a boundary case when EXACTLY limit files were in commit, but I do ignore it + //because otherwise getFilesInCommit API would have to be changed. + commit_is_trimmed = (paths.size()==paths_limit); + }; + if (isTraceEnabled()) logger().trace("Finished loading changed paths "+paths.size()+" paths, "+(commit_is_trimmed ? " commit list is TRIMMED to web.maxCommitPaths":"")); // add commit diffstat int insertions = 0; int deletions = 0; @@ -163,9 +182,9 @@ public void populateItem(final Item item) { insertions += pcm.insertions; deletions += pcm.deletions; } - add(new DiffStatPanel("diffStat", insertions, deletions)); + add(new DiffStatPanel("diffStat", insertions, deletions, false, commit_is_trimmed)); - add(new CommitLegendPanel("commitLegend", paths)); + add(new CommitLegendPanel("commitLegend", paths, commit_is_trimmed)); ListDataProvider pathsDp = new ListDataProvider(paths); DataView pathsView = new DataView("changedPath", pathsDp) { private static final long serialVersionUID = 1L; @@ -173,6 +192,7 @@ public void populateItem(final Item item) { @Override public void populateItem(final Item item) { + if (isTraceEnabled()) logger().trace("Polulating commit path item "+item.getModelObject()); final PathChangeModel entry = item.getModelObject(); Label changeType = new Label("changeType", ""); diff --git a/src/main/java/com/gitblit/wicket/panels/CommitLegendPanel.html b/src/main/java/com/gitblit/wicket/panels/CommitLegendPanel.html index 71063626c..54bd9f791 100644 --- a/src/main/java/com/gitblit/wicket/panels/CommitLegendPanel.html +++ b/src/main/java/com/gitblit/wicket/panels/CommitLegendPanel.html @@ -5,9 +5,10 @@ lang="en"> +
[more changes]
[change type] [description] -
+
\ No newline at end of file diff --git a/src/main/java/com/gitblit/wicket/panels/CommitLegendPanel.java b/src/main/java/com/gitblit/wicket/panels/CommitLegendPanel.java index 2b7fa91f5..7a08fa230 100644 --- a/src/main/java/com/gitblit/wicket/panels/CommitLegendPanel.java +++ b/src/main/java/com/gitblit/wicket/panels/CommitLegendPanel.java @@ -43,9 +43,16 @@ public class CommitLegendPanel extends Panel { static final boolean TRACE = LOGGER.isTraceEnabled(); static final boolean DEBUG = LOGGER.isDebugEnabled(); - - public CommitLegendPanel(String id, List paths) { + public CommitLegendPanel(String id, List paths) { this(id, paths, false) ;}; + /** Creates + @param id wicket idendtifier + @param paths list of changed paths + @param approx true if paths do not represent a full set of changes + present in commit due to, for an example, some limits. + */ + public CommitLegendPanel(String id, List paths, final boolean approx) { super(id); + if (TRACE) LOGGER.trace("new CommitLegendPanel("+id+",paths.size()="+paths.size()+", approx="+approx+")"); final Map stats = getChangedPathsStats(paths); ListDataProvider legendDp = new ListDataProvider( new ArrayList(stats.keySet())); @@ -81,7 +88,11 @@ public void populateItem(final Item item) { item.add(new Label("description", description)); } }; - add(legendsView); + //Note: There is a known problem which I can't handle, that is "moreChanges" do appear + //on web BEFORE "legend". + add(new Label("moreChanges",getString("gb.CommitLegendPanel.moreChanges")).setVisible(approx)); + add(legendsView); + } protected Map getChangedPathsStats(List paths) { diff --git a/src/main/java/com/gitblit/wicket/panels/DiffStatPanel.java b/src/main/java/com/gitblit/wicket/panels/DiffStatPanel.java index 30a89027e..c836178bd 100644 --- a/src/main/java/com/gitblit/wicket/panels/DiffStatPanel.java +++ b/src/main/java/com/gitblit/wicket/panels/DiffStatPanel.java @@ -20,6 +20,9 @@ import org.apache.wicket.markup.html.basic.Label; import org.apache.wicket.markup.html.panel.Panel; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + import com.gitblit.utils.DiffUtils; import com.gitblit.utils.DiffUtils.NormalizedDiffStat; import com.gitblit.wicket.WicketUtils; @@ -33,6 +36,10 @@ public class DiffStatPanel extends Panel { private static final long serialVersionUID = 1L; + static final Logger LOGGER = LoggerFactory.getLogger(DiffStatPanel.class); + static final boolean TRACE = LOGGER.isTraceEnabled(); + static final boolean DEBUG = LOGGER.isDebugEnabled(); + final int total; @@ -41,13 +48,28 @@ public class DiffStatPanel extends Panel { final int deletions; final boolean inline; - + + final boolean approx; + public DiffStatPanel(String wicketId, int insertions, int deletions) { this(wicketId, insertions, deletions, false); } - public DiffStatPanel(String wicketId, int insertions, int deletions, boolean inline) { + this(wicketId, insertions, deletions, inline,false); + }; + /** Creates + @param wicketId wicket identifier + @param insertions total number of insertions within a commit. + @param deletions total number of deletions within a commit. + @param inline true if panel is inline with commit file and represents stats of single file + or flase if used in header and represents stats of commit. + @param approx true if number of insertions and deletions is approximate, ie because + commit processing was limited in server settings. + */ + public DiffStatPanel(String wicketId, int insertions, int deletions, boolean inline, boolean approx) { super(wicketId); + if (TRACE) LOGGER.trace("new DiffStatPanel("+wicketId+",insertions="+insertions+",deletions="+deletions+",inline="+inline+",approx="+approx); + this.approx = approx; this.insertions = insertions; this.deletions = deletions; this.total = insertions + deletions; @@ -56,9 +78,11 @@ public DiffStatPanel(String wicketId, int insertions, int deletions, boolean inl @Override protected void onInitialize() { + if (TRACE) LOGGER.trace("DiffStatPanel.onInitialize() ENTER"); + super.onInitialize(); - - final String diffStat = MessageFormat.format(getString("gb.diffStat"), "" + insertions, "" + deletions); + + final String diffStat = MessageFormat.format(getString("gb.diffStat"), "" + insertions, "" + deletions, approx ? 1 :0 ); WicketUtils.setHtmlTooltip(this, diffStat); final NormalizedDiffStat n = DiffUtils.normalizeDiffStat(5, insertions, deletions); @@ -69,8 +93,8 @@ protected void onInitialize() { } else { segment = "■"; } - - add(new Label("total", String.valueOf(total))); + + add(new Label("total",MessageFormat.format(getString("gb.diffStat.total"),String.valueOf(total), approx ? 1 :0))); add(new Label("insertions", timesRepeat(n.insertions, segment)).setEscapeModelStrings(false).setVisible(n.insertions > 0)); add(new Label("deletions", timesRepeat(n.deletions, segment)).setEscapeModelStrings(false).setVisible(n.deletions > 0)); add(new Label("blank", timesRepeat(n.blanks, segment)).setEscapeModelStrings(false).setVisible(n.blanks > 0)); @@ -82,6 +106,8 @@ protected void onInitialize() { } setVisible(total > 0); + + if (TRACE) LOGGER.trace("DiffStatPanel.onInitialize() LEAVE"); } String timesRepeat(int cnt, String s) { From 3f83aafd86f670f7d5613944fd3b4019e89efbbd Mon Sep 17 00:00:00 2001 From: Tomasz Sztejka Date: Thu, 4 Nov 2021 15:57:51 +0100 Subject: [PATCH 19/21] 1.Fixed comment in panel 2.Refactored GitBlitServer main() method to serve for tests where tests do need to run main related code before the instance of server is created. This seems to be a design flaw in it since some elements are processed before and some inside constructor. Sadly the way of loggers configuration enforces such a trick. --- src/main/java/com/gitblit/GitBlitServer.java | 21 ++++++++-- .../wicket/panels/CommitLegendPanel.java | 2 +- .../devops/GitBlitServer4UITests.java | 40 +------------------ 3 files changed, 20 insertions(+), 43 deletions(-) diff --git a/src/main/java/com/gitblit/GitBlitServer.java b/src/main/java/com/gitblit/GitBlitServer.java index 80fd6cca5..47913acdb 100644 --- a/src/main/java/com/gitblit/GitBlitServer.java +++ b/src/main/java/com/gitblit/GitBlitServer.java @@ -86,8 +86,19 @@ public class GitBlitServer { private static Logger logger; - - public static void main(String... args) { + public static void main(String... args)throws Exception { + main(GitBlitServer.class, args); + } + /** + A main method which is declated in such a way, that + exactly the same per-processing of parameters can + be used in subclasses which are designed for testing + purposes. + + @see de.akquinet.devops.GitBlitServer4UITests + */ + protected static void main( Class impl, String... args)throws Exception + { // filter out the baseFolder parameter List filtered = new ArrayList(); String folder = "data"; @@ -141,10 +152,12 @@ public static void main(String... args) { //Set up logging. setUpLogging(settings, params); - GitBlitServer server = new GitBlitServer(); + GitBlitServer server = impl.newInstance(); server.start(params, settings); } - } + }; + + /** * Display the command line usage of Gitblit GO. diff --git a/src/main/java/com/gitblit/wicket/panels/CommitLegendPanel.java b/src/main/java/com/gitblit/wicket/panels/CommitLegendPanel.java index 7a08fa230..bba52df4f 100644 --- a/src/main/java/com/gitblit/wicket/panels/CommitLegendPanel.java +++ b/src/main/java/com/gitblit/wicket/panels/CommitLegendPanel.java @@ -89,7 +89,7 @@ public void populateItem(final Item item) { } }; //Note: There is a known problem which I can't handle, that is "moreChanges" do appear - //on web BEFORE "legend". + //on web BEFORE "legend" if in html is AFTER "description". add(new Label("moreChanges",getString("gb.CommitLegendPanel.moreChanges")).setVisible(approx)); add(legendsView); diff --git a/src/test/java/de/akquinet/devops/GitBlitServer4UITests.java b/src/test/java/de/akquinet/devops/GitBlitServer4UITests.java index 8d6d703b2..d00a22517 100644 --- a/src/test/java/de/akquinet/devops/GitBlitServer4UITests.java +++ b/src/test/java/de/akquinet/devops/GitBlitServer4UITests.java @@ -14,44 +14,8 @@ public class GitBlitServer4UITests extends GitBlitServer { - public static void main(String... args) { - GitBlitServer4UITests server = new GitBlitServer4UITests(); - - // filter out the baseFolder parameter - List filtered = new ArrayList(); - String folder = "data"; - for (int i = 0; i < args.length; i++) { - String arg = args[i]; - if (arg.equals("--baseFolder")) { - if (i + 1 == args.length) { - System.out.println("Invalid --baseFolder parameter!"); - System.exit(-1); - } else if (args[i + 1] != ".") { - folder = args[i + 1]; - } - i = i + 1; - } else { - filtered.add(arg); - } - } - - Params.baseFolder = folder; - Params params = new Params(); - CmdLineParser parser = new CmdLineParser(params); - try { - parser.parseArgument(filtered); - if (params.help) { - server.usage(parser, null); - } - } catch (CmdLineException t) { - server.usage(parser, t); - } - - if (params.stop) { - server.stop(params); - } else { - server.start(params); - } + public static void main(String... args)throws Exception { + GitBlitServer.main(GitBlitServer4UITests.class, args); } @Override From 44fc7c164f7b5d6f7a4194bd87b3fbb131541fdf Mon Sep 17 00:00:00 2001 From: Tomasz Sztejka Date: Thu, 4 Nov 2021 16:06:26 +0100 Subject: [PATCH 20/21] Detected some test errors after ant test but after checking out master noticed, that those are also present in master build and are not related to changes made. Damn... I do not feel right to dig into it. --- src/main/java/com/gitblit/GitBlitServer.java | 12 +++++++----- .../de/akquinet/devops/GitBlitServer4UITests.java | 2 +- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/src/main/java/com/gitblit/GitBlitServer.java b/src/main/java/com/gitblit/GitBlitServer.java index 47913acdb..0250d5d33 100644 --- a/src/main/java/com/gitblit/GitBlitServer.java +++ b/src/main/java/com/gitblit/GitBlitServer.java @@ -86,7 +86,7 @@ public class GitBlitServer { private static Logger logger; - public static void main(String... args)throws Exception { + public static void main(String... args) { main(GitBlitServer.class, args); } /** @@ -97,7 +97,7 @@ public static void main(String... args)throws Exception { @see de.akquinet.devops.GitBlitServer4UITests */ - protected static void main( Class impl, String... args)throws Exception + protected static void main( Class impl, String... args) { // filter out the baseFolder parameter List filtered = new ArrayList(); @@ -151,9 +151,11 @@ protected static void main( Class impl, String... args) } //Set up logging. setUpLogging(settings, params); - - GitBlitServer server = impl.newInstance(); - server.start(params, settings); + try{ + GitBlitServer server = impl.newInstance(); + server.start(params, settings); + }catch(InstantiationException ex){ throw new RuntimeException(ex); } + catch(IllegalAccessException ex){ throw new RuntimeException(ex); } } }; diff --git a/src/test/java/de/akquinet/devops/GitBlitServer4UITests.java b/src/test/java/de/akquinet/devops/GitBlitServer4UITests.java index d00a22517..215ca516b 100644 --- a/src/test/java/de/akquinet/devops/GitBlitServer4UITests.java +++ b/src/test/java/de/akquinet/devops/GitBlitServer4UITests.java @@ -14,7 +14,7 @@ public class GitBlitServer4UITests extends GitBlitServer { - public static void main(String... args)throws Exception { + public static void main(String... args) { GitBlitServer.main(GitBlitServer4UITests.class, args); } From 2bcad9b36491c60cd3a0ecc1e8415a3247ab58b1 Mon Sep 17 00:00:00 2001 From: Tomasz Sztejka Date: Fri, 5 Nov 2021 14:27:12 +0100 Subject: [PATCH 21/21] 1.Fixed gitblit issue https://github.com/gitblit/gitblit/issues/1389 regarding peculiar problem after server upgrade when web.maxActivityCommits is modified in server configuration to not be one of -1, 0, 25, 50, 75, 100, 150, 200, 250, 500. The detailed and exact code line which is responsible for why this happens is something beond my understanding. --- build.moxie | 2 +- .../gitblit/wicket/pages/EditRepositoryPage.java | 16 ++++++++++++++++ 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/build.moxie b/build.moxie index 41cee37b0..fe7cf446b 100644 --- a/build.moxie +++ b/build.moxie @@ -10,7 +10,7 @@ name: Gitblit description: pure Java Git solution groupId: com.gitblit artifactId: gitblit -version: 1.9.2-TomaszSzt.1.0 +version: 1.9.2-TomaszSzt.1.01 inceptionYear: 2011 # Current stable release diff --git a/src/main/java/com/gitblit/wicket/pages/EditRepositoryPage.java b/src/main/java/com/gitblit/wicket/pages/EditRepositoryPage.java index bf3eea8ba..510ed2765 100644 --- a/src/main/java/com/gitblit/wicket/pages/EditRepositoryPage.java +++ b/src/main/java/com/gitblit/wicket/pages/EditRepositoryPage.java @@ -570,7 +570,23 @@ protected void onSubmit() { getString("gb.skipSummaryMetricsDescription"), new PropertyModel(repositoryModel, "skipSummaryMetrics"))); + //We need to prepare list of pre-defined values and include the currently stored value. List maxActivityCommits = Arrays.asList(-1, 0, 25, 50, 75, 100, 150, 200, 250, 500); + { + Integer current = repositoryModel.maxActivityCommits; + if (!maxActivityCommits.contains(current)) + { + //insert it in a apropriate location. The easiest by laughably costly way + //is to just add and sort. This is is not a problem, since this type of + //action will be necessary only when server setup was modified in such a way, + //that default value of web.maxActivityCommits is not on the allowed above + //list. Why? Bite me. + maxActivityCommits = new ArrayList(maxActivityCommits); + maxActivityCommits.add(current); + Collections.sort(maxActivityCommits); + }; + }; + form.add(new ChoiceOption("maxActivityCommits", getString("gb.maxActivityCommits"), getString("gb.maxActivityCommitsDescription"),