summaryrefslogtreecommitdiffstats
path: root/src/main/java/com/gitblit/utils
diff options
context:
space:
mode:
Diffstat (limited to 'src/main/java/com/gitblit/utils')
-rw-r--r--src/main/java/com/gitblit/utils/ActivityUtils.java205
-rw-r--r--src/main/java/com/gitblit/utils/ArrayUtils.java74
-rw-r--r--src/main/java/com/gitblit/utils/Base64.java311
-rw-r--r--src/main/java/com/gitblit/utils/ByteFormat.java65
-rw-r--r--src/main/java/com/gitblit/utils/ClientLogger.java77
-rw-r--r--src/main/java/com/gitblit/utils/CompressionUtils.java299
-rw-r--r--src/main/java/com/gitblit/utils/ConnectionUtils.java214
-rw-r--r--src/main/java/com/gitblit/utils/ContainerUtils.java135
-rw-r--r--src/main/java/com/gitblit/utils/DeepCopier.java135
-rw-r--r--src/main/java/com/gitblit/utils/DiffUtils.java281
-rw-r--r--src/main/java/com/gitblit/utils/FederationUtils.java349
-rw-r--r--src/main/java/com/gitblit/utils/FileUtils.java292
-rw-r--r--src/main/java/com/gitblit/utils/GitBlitDiffFormatter.java164
-rw-r--r--src/main/java/com/gitblit/utils/GitWebDiffFormatter.java155
-rw-r--r--src/main/java/com/gitblit/utils/HttpUtils.java204
-rw-r--r--src/main/java/com/gitblit/utils/IssueUtils.java829
-rw-r--r--src/main/java/com/gitblit/utils/JGitUtils.java1775
-rw-r--r--src/main/java/com/gitblit/utils/JsonUtils.java346
-rw-r--r--src/main/java/com/gitblit/utils/MarkdownUtils.java87
-rw-r--r--src/main/java/com/gitblit/utils/MetricUtils.java233
-rw-r--r--src/main/java/com/gitblit/utils/ObjectCache.java98
-rw-r--r--src/main/java/com/gitblit/utils/PatchFormatter.java143
-rw-r--r--src/main/java/com/gitblit/utils/PushLogUtils.java344
-rw-r--r--src/main/java/com/gitblit/utils/RpcUtils.java637
-rw-r--r--src/main/java/com/gitblit/utils/StringUtils.java736
-rw-r--r--src/main/java/com/gitblit/utils/SyndicationUtils.java262
-rw-r--r--src/main/java/com/gitblit/utils/TicgitUtils.java148
-rw-r--r--src/main/java/com/gitblit/utils/TimeUtils.java341
-rw-r--r--src/main/java/com/gitblit/utils/X509Utils.java1136
29 files changed, 10075 insertions, 0 deletions
diff --git a/src/main/java/com/gitblit/utils/ActivityUtils.java b/src/main/java/com/gitblit/utils/ActivityUtils.java
new file mode 100644
index 00000000..732fdeb1
--- /dev/null
+++ b/src/main/java/com/gitblit/utils/ActivityUtils.java
@@ -0,0 +1,205 @@
+/*
+ * 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.FileNotFoundException;
+import java.io.IOException;
+import java.lang.reflect.Type;
+import java.text.DateFormat;
+import java.text.MessageFormat;
+import java.text.SimpleDateFormat;
+import java.util.ArrayList;
+import java.util.Calendar;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.TimeZone;
+
+import org.eclipse.jgit.lib.Constants;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.revwalk.RevCommit;
+
+import com.gitblit.GitBlit;
+import com.gitblit.models.Activity;
+import com.gitblit.models.GravatarProfile;
+import com.gitblit.models.RefModel;
+import com.gitblit.models.RepositoryCommit;
+import com.gitblit.models.RepositoryModel;
+import com.google.gson.reflect.TypeToken;
+
+/**
+ * Utility class for building activity information from repositories.
+ *
+ * @author James Moger
+ *
+ */
+public class ActivityUtils {
+
+ /**
+ * Gets the recent activity from the repositories for the last daysBack days
+ * on the specified branch.
+ *
+ * @param models
+ * the list of repositories to query
+ * @param daysBack
+ * the number of days back from Now to collect
+ * @param objectId
+ * the branch to retrieve. If this value is null or empty all
+ * branches are queried.
+ * @param timezone
+ * the timezone for aggregating commits
+ * @return
+ */
+ public static List<Activity> getRecentActivity(List<RepositoryModel> models, int daysBack,
+ String objectId, TimeZone timezone) {
+
+ // Activity panel shows last daysBack of activity across all
+ // repositories.
+ Date thresholdDate = new Date(System.currentTimeMillis() - daysBack * TimeUtils.ONEDAY);
+
+ // Build a map of DailyActivity from the available repositories for the
+ // specified threshold date.
+ DateFormat df = new SimpleDateFormat("yyyy-MM-dd");
+ df.setTimeZone(timezone);
+ Calendar cal = Calendar.getInstance();
+ cal.setTimeZone(timezone);
+
+ Map<String, Activity> activity = new HashMap<String, Activity>();
+ for (RepositoryModel model : models) {
+ if (model.maxActivityCommits == -1) {
+ // skip this repository
+ continue;
+ }
+ if (model.hasCommits && model.lastChange.after(thresholdDate)) {
+ if (model.isCollectingGarbage) {
+ continue;
+ }
+ Repository repository = GitBlit.self()
+ .getRepository(model.name);
+ List<String> branches = new ArrayList<String>();
+ if (StringUtils.isEmpty(objectId)) {
+ for (RefModel local : JGitUtils.getLocalBranches(
+ repository, true, -1)) {
+ branches.add(local.getName());
+ }
+ } else {
+ branches.add(objectId);
+ }
+ Map<ObjectId, List<RefModel>> allRefs = JGitUtils
+ .getAllRefs(repository, model.showRemoteBranches);
+
+ for (String branch : branches) {
+ String shortName = branch;
+ if (shortName.startsWith(Constants.R_HEADS)) {
+ shortName = shortName.substring(Constants.R_HEADS.length());
+ }
+ List<RevCommit> commits = JGitUtils.getRevLog(repository,
+ branch, thresholdDate);
+ if (model.maxActivityCommits > 0 && commits.size() > model.maxActivityCommits) {
+ // trim commits to maximum count
+ commits = commits.subList(0, model.maxActivityCommits);
+ }
+ for (RevCommit commit : commits) {
+ Date date = JGitUtils.getCommitDate(commit);
+ String dateStr = df.format(date);
+ if (!activity.containsKey(dateStr)) {
+ // Normalize the date to midnight
+ cal.setTime(date);
+ cal.set(Calendar.HOUR_OF_DAY, 0);
+ cal.set(Calendar.MINUTE, 0);
+ cal.set(Calendar.SECOND, 0);
+ cal.set(Calendar.MILLISECOND, 0);
+ activity.put(dateStr, new Activity(cal.getTime()));
+ }
+ RepositoryCommit commitModel = activity.get(dateStr)
+ .addCommit(model.name, shortName, commit);
+ if (commitModel != null) {
+ commitModel.setRefs(allRefs.get(commit.getId()));
+ }
+ }
+ }
+
+ // close the repository
+ repository.close();
+ }
+ }
+
+ List<Activity> recentActivity = new ArrayList<Activity>(activity.values());
+ return recentActivity;
+ }
+
+ /**
+ * Returns the Gravatar profile, if available, for the specified email
+ * address.
+ *
+ * @param emailaddress
+ * @return a Gravatar Profile
+ * @throws IOException
+ */
+ public static GravatarProfile getGravatarProfileFromAddress(String emailaddress)
+ throws IOException {
+ return getGravatarProfile(StringUtils.getMD5(emailaddress.toLowerCase()));
+ }
+
+ /**
+ * Creates a Gravatar thumbnail url from the specified email address.
+ *
+ * @param email
+ * address to query Gravatar
+ * @param width
+ * size of thumbnail. if width <= 0, the default of 50 is used.
+ * @return
+ */
+ public static String getGravatarThumbnailUrl(String email, int width) {
+ if (width <= 0) {
+ width = 50;
+ }
+ String emailHash = StringUtils.getMD5(email);
+ String url = MessageFormat.format(
+ "https://www.gravatar.com/avatar/{0}?s={1,number,0}&d=identicon", emailHash, width);
+ return url;
+ }
+
+ /**
+ * Returns the Gravatar profile, if available, for the specified hashcode.
+ * address.
+ *
+ * @param hash
+ * the hash of the email address
+ * @return a Gravatar Profile
+ * @throws IOException
+ */
+ public static GravatarProfile getGravatarProfile(String hash) throws IOException {
+ String url = MessageFormat.format("https://www.gravatar.com/{0}.json", hash);
+ // Gravatar has a complex json structure
+ Type profileType = new TypeToken<Map<String, List<GravatarProfile>>>() {
+ }.getType();
+ Map<String, List<GravatarProfile>> profiles = null;
+ try {
+ profiles = JsonUtils.retrieveJson(url, profileType);
+ } catch (FileNotFoundException e) {
+ }
+ if (profiles == null || profiles.size() == 0) {
+ return null;
+ }
+ // due to the complex json structure we need to pull out the profile
+ // from a list 2 levels deep
+ GravatarProfile profile = profiles.values().iterator().next().get(0);
+ return profile;
+ }
+}
diff --git a/src/main/java/com/gitblit/utils/ArrayUtils.java b/src/main/java/com/gitblit/utils/ArrayUtils.java
new file mode 100644
index 00000000..65834673
--- /dev/null
+++ b/src/main/java/com/gitblit/utils/ArrayUtils.java
@@ -0,0 +1,74 @@
+/*
+ * Copyright 2012 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.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+
+
+/**
+ * Utility class for arrays and collections.
+ *
+ * @author James Moger
+ *
+ */
+public class ArrayUtils {
+
+ public static boolean isEmpty(byte [] array) {
+ return array == null || array.length == 0;
+ }
+
+ public static boolean isEmpty(char [] array) {
+ return array == null || array.length == 0;
+ }
+
+ public static boolean isEmpty(Object [] array) {
+ return array == null || array.length == 0;
+ }
+
+ public static boolean isEmpty(Collection<?> collection) {
+ return collection == null || collection.size() == 0;
+ }
+
+ public static String toString(Collection<?> collection) {
+ if (isEmpty(collection)) {
+ return "";
+ }
+ StringBuilder sb = new StringBuilder();
+ for (Object o : collection) {
+ sb.append(o.toString()).append(", ");
+ }
+ // trim trailing comma-space
+ sb.setLength(sb.length() - 2);
+ return sb.toString();
+ }
+
+ public static Collection<String> fromString(String value) {
+ if (StringUtils.isEmpty(value)) {
+ value = "";
+ }
+ List<String> list = new ArrayList<String>();
+ String [] values = value.split(",|;");
+ for (String v : values) {
+ String string = v.trim();
+ if (!StringUtils.isEmpty(string)) {
+ list.add(string);
+ }
+ }
+ return list;
+ }
+}
diff --git a/src/main/java/com/gitblit/utils/Base64.java b/src/main/java/com/gitblit/utils/Base64.java
new file mode 100644
index 00000000..6fd2daf1
--- /dev/null
+++ b/src/main/java/com/gitblit/utils/Base64.java
@@ -0,0 +1,311 @@
+//
+// NOTE: The following source code is heavily derived from the
+// iHarder.net public domain Base64 library. See the original at
+// http://iharder.sourceforge.net/current/java/base64/
+//
+
+package com.gitblit.utils;
+
+import java.io.UnsupportedEncodingException;
+import java.text.MessageFormat;
+import java.util.Arrays;
+
+/**
+ * Encodes and decodes to and from Base64 notation.
+ * <p>
+ * I am placing this code in the Public Domain. Do with it as you will. This
+ * software comes with no guarantees or warranties but with plenty of
+ * well-wishing instead! Please visit <a
+ * href="http://iharder.net/base64">http://iharder.net/base64</a> periodically
+ * to check for updates or to contribute improvements.
+ * </p>
+ *
+ * @author Robert Harder
+ * @author rob@iharder.net
+ * @version 2.1, stripped to minimum feature set used by JGit.
+ */
+public class Base64 {
+ /** The equals sign (=) as a byte. */
+ private final static byte EQUALS_SIGN = (byte) '=';
+
+ /** Indicates equals sign in encoding. */
+ private final static byte EQUALS_SIGN_DEC = -1;
+
+ /** Indicates white space in encoding. */
+ private final static byte WHITE_SPACE_DEC = -2;
+
+ /** Indicates an invalid byte during decoding. */
+ private final static byte INVALID_DEC = -3;
+
+ /** Preferred encoding. */
+ private final static String UTF_8 = "UTF-8";
+
+ /** The 64 valid Base64 values. */
+ private final static byte[] ENC;
+
+ /**
+ * Translates a Base64 value to either its 6-bit reconstruction value or a
+ * negative number indicating some other meaning. The table is only 7 bits
+ * wide, as the 8th bit is discarded during decoding.
+ */
+ private final static byte[] DEC;
+
+ static {
+ try {
+ ENC = ("ABCDEFGHIJKLMNOPQRSTUVWXYZ" //
+ + "abcdefghijklmnopqrstuvwxyz" //
+ + "0123456789" //
+ + "+/" //
+ ).getBytes(UTF_8);
+ } catch (UnsupportedEncodingException uee) {
+ throw new RuntimeException(uee.getMessage(), uee);
+ }
+
+ DEC = new byte[128];
+ Arrays.fill(DEC, INVALID_DEC);
+
+ for (int i = 0; i < 64; i++)
+ DEC[ENC[i]] = (byte) i;
+ DEC[EQUALS_SIGN] = EQUALS_SIGN_DEC;
+
+ DEC['\t'] = WHITE_SPACE_DEC;
+ DEC['\n'] = WHITE_SPACE_DEC;
+ DEC['\r'] = WHITE_SPACE_DEC;
+ DEC[' '] = WHITE_SPACE_DEC;
+ }
+
+ /** Defeats instantiation. */
+ private Base64() {
+ // Suppress empty block warning.
+ }
+
+ /**
+ * Encodes up to three bytes of the array <var>source</var> and writes the
+ * resulting four Base64 bytes to <var>destination</var>. The source and
+ * destination arrays can be manipulated anywhere along their length by
+ * specifying <var>srcOffset</var> and <var>destOffset</var>. This method
+ * does not check to make sure your arrays are large enough to accommodate
+ * <var>srcOffset</var> + 3 for the <var>source</var> array or
+ * <var>destOffset</var> + 4 for the <var>destination</var> array. The
+ * actual number of significant bytes in your array is given by
+ * <var>numSigBytes</var>.
+ *
+ * @param source
+ * the array to convert
+ * @param srcOffset
+ * the index where conversion begins
+ * @param numSigBytes
+ * the number of significant bytes in your array
+ * @param destination
+ * the array to hold the conversion
+ * @param destOffset
+ * the index where output will be put
+ */
+ private static void encode3to4(byte[] source, int srcOffset, int numSigBytes,
+ byte[] destination, int destOffset) {
+ // We have to shift left 24 in order to flush out the 1's that appear
+ // when Java treats a value as negative that is cast from a byte.
+
+ int inBuff = 0;
+ switch (numSigBytes) {
+ case 3:
+ inBuff |= (source[srcOffset + 2] << 24) >>> 24;
+ //$FALL-THROUGH$
+
+ case 2:
+ inBuff |= (source[srcOffset + 1] << 24) >>> 16;
+ //$FALL-THROUGH$
+
+ case 1:
+ inBuff |= (source[srcOffset] << 24) >>> 8;
+ }
+
+ switch (numSigBytes) {
+ case 3:
+ destination[destOffset] = ENC[(inBuff >>> 18)];
+ destination[destOffset + 1] = ENC[(inBuff >>> 12) & 0x3f];
+ destination[destOffset + 2] = ENC[(inBuff >>> 6) & 0x3f];
+ destination[destOffset + 3] = ENC[(inBuff) & 0x3f];
+ break;
+
+ case 2:
+ destination[destOffset] = ENC[(inBuff >>> 18)];
+ destination[destOffset + 1] = ENC[(inBuff >>> 12) & 0x3f];
+ destination[destOffset + 2] = ENC[(inBuff >>> 6) & 0x3f];
+ destination[destOffset + 3] = EQUALS_SIGN;
+ break;
+
+ case 1:
+ destination[destOffset] = ENC[(inBuff >>> 18)];
+ destination[destOffset + 1] = ENC[(inBuff >>> 12) & 0x3f];
+ destination[destOffset + 2] = EQUALS_SIGN;
+ destination[destOffset + 3] = EQUALS_SIGN;
+ break;
+ }
+ }
+
+ /**
+ * Encodes a byte array into Base64 notation.
+ *
+ * @param source
+ * The data to convert
+ * @return encoded base64 representation of source.
+ */
+ public static String encodeBytes(byte[] source) {
+ return encodeBytes(source, 0, source.length);
+ }
+
+ /**
+ * Encodes a byte array into Base64 notation.
+ *
+ * @param source
+ * The data to convert
+ * @param off
+ * Offset in array where conversion should begin
+ * @param len
+ * Length of data to convert
+ * @return encoded base64 representation of source.
+ */
+ public static String encodeBytes(byte[] source, int off, int len) {
+ final int len43 = len * 4 / 3;
+
+ byte[] outBuff = new byte[len43 + ((len % 3) > 0 ? 4 : 0)];
+ int d = 0;
+ int e = 0;
+ int len2 = len - 2;
+
+ for (; d < len2; d += 3, e += 4)
+ encode3to4(source, d + off, 3, outBuff, e);
+
+ if (d < len) {
+ encode3to4(source, d + off, len - d, outBuff, e);
+ e += 4;
+ }
+
+ try {
+ return new String(outBuff, 0, e, UTF_8);
+ } catch (UnsupportedEncodingException uue) {
+ return new String(outBuff, 0, e);
+ }
+ }
+
+ /**
+ * Decodes four bytes from array <var>source</var> and writes the resulting
+ * bytes (up to three of them) to <var>destination</var>. The source and
+ * destination arrays can be manipulated anywhere along their length by
+ * specifying <var>srcOffset</var> and <var>destOffset</var>. This method
+ * does not check to make sure your arrays are large enough to accommodate
+ * <var>srcOffset</var> + 4 for the <var>source</var> array or
+ * <var>destOffset</var> + 3 for the <var>destination</var> array. This
+ * method returns the actual number of bytes that were converted from the
+ * Base64 encoding.
+ *
+ * @param source
+ * the array to convert
+ * @param srcOffset
+ * the index where conversion begins
+ * @param destination
+ * the array to hold the conversion
+ * @param destOffset
+ * the index where output will be put
+ * @return the number of decoded bytes converted
+ */
+ private static int decode4to3(byte[] source, int srcOffset, byte[] destination, int destOffset) {
+ // Example: Dk==
+ if (source[srcOffset + 2] == EQUALS_SIGN) {
+ int outBuff = ((DEC[source[srcOffset]] & 0xFF) << 18)
+ | ((DEC[source[srcOffset + 1]] & 0xFF) << 12);
+ destination[destOffset] = (byte) (outBuff >>> 16);
+ return 1;
+ }
+
+ // Example: DkL=
+ else if (source[srcOffset + 3] == EQUALS_SIGN) {
+ int outBuff = ((DEC[source[srcOffset]] & 0xFF) << 18)
+ | ((DEC[source[srcOffset + 1]] & 0xFF) << 12)
+ | ((DEC[source[srcOffset + 2]] & 0xFF) << 6);
+ destination[destOffset] = (byte) (outBuff >>> 16);
+ destination[destOffset + 1] = (byte) (outBuff >>> 8);
+ return 2;
+ }
+
+ // Example: DkLE
+ else {
+ int outBuff = ((DEC[source[srcOffset]] & 0xFF) << 18)
+ | ((DEC[source[srcOffset + 1]] & 0xFF) << 12)
+ | ((DEC[source[srcOffset + 2]] & 0xFF) << 6)
+ | ((DEC[source[srcOffset + 3]] & 0xFF));
+
+ destination[destOffset] = (byte) (outBuff >> 16);
+ destination[destOffset + 1] = (byte) (outBuff >> 8);
+ destination[destOffset + 2] = (byte) (outBuff);
+
+ return 3;
+ }
+ }
+
+ /**
+ * Low-level decoding ASCII characters from a byte array.
+ *
+ * @param source
+ * The Base64 encoded data
+ * @param off
+ * The offset of where to begin decoding
+ * @param len
+ * The length of characters to decode
+ * @return decoded data
+ * @throws IllegalArgumentException
+ * the input is not a valid Base64 sequence.
+ */
+ public static byte[] decode(byte[] source, int off, int len) {
+ byte[] outBuff = new byte[len * 3 / 4]; // Upper limit on size of output
+ int outBuffPosn = 0;
+
+ byte[] b4 = new byte[4];
+ int b4Posn = 0;
+
+ for (int i = off; i < off + len; i++) {
+ byte sbiCrop = (byte) (source[i] & 0x7f);
+ byte sbiDecode = DEC[sbiCrop];
+
+ if (EQUALS_SIGN_DEC <= sbiDecode) {
+ b4[b4Posn++] = sbiCrop;
+ if (b4Posn > 3) {
+ outBuffPosn += decode4to3(b4, 0, outBuff, outBuffPosn);
+ b4Posn = 0;
+
+ // If that was the equals sign, break out of 'for' loop
+ if (sbiCrop == EQUALS_SIGN)
+ break;
+ }
+
+ } else if (sbiDecode != WHITE_SPACE_DEC)
+ throw new IllegalArgumentException(MessageFormat.format(
+ "bad base64 input character {1} at {0}", i, source[i] & 0xff));
+ }
+
+ if (outBuff.length == outBuffPosn)
+ return outBuff;
+
+ byte[] out = new byte[outBuffPosn];
+ System.arraycopy(outBuff, 0, out, 0, outBuffPosn);
+ return out;
+ }
+
+ /**
+ * Decodes data from Base64 notation.
+ *
+ * @param s
+ * the string to decode
+ * @return the decoded data
+ */
+ public static byte[] decode(String s) {
+ byte[] bytes;
+ try {
+ bytes = s.getBytes(UTF_8);
+ } catch (UnsupportedEncodingException uee) {
+ bytes = s.getBytes();
+ }
+ return decode(bytes, 0, bytes.length);
+ }
+}
diff --git a/src/main/java/com/gitblit/utils/ByteFormat.java b/src/main/java/com/gitblit/utils/ByteFormat.java
new file mode 100644
index 00000000..cb7da885
--- /dev/null
+++ b/src/main/java/com/gitblit/utils/ByteFormat.java
@@ -0,0 +1,65 @@
+/*
+ * 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.text.DecimalFormat;
+import java.text.FieldPosition;
+import java.text.Format;
+import java.text.ParsePosition;
+
+/**
+ * ByteFormat is a formatter which takes numbers and returns filesizes in bytes,
+ * kilobytes, megabytes, or gigabytes.
+ *
+ * @author James Moger
+ *
+ */
+public class ByteFormat extends Format {
+
+ private static final long serialVersionUID = 1L;
+
+ public ByteFormat() {
+ }
+
+ public String format(long value) {
+ return format(Long.valueOf(value));
+ }
+
+ public StringBuffer format(Object obj, StringBuffer buf, FieldPosition pos) {
+ if (obj instanceof Number) {
+ long numBytes = ((Number) obj).longValue();
+ if (numBytes < 1024) {
+ DecimalFormat formatter = new DecimalFormat("#,##0");
+ buf.append(formatter.format((double) numBytes)).append(" b");
+ } else if (numBytes < 1024 * 1024) {
+ DecimalFormat formatter = new DecimalFormat("#,##0");
+ buf.append(formatter.format((double) numBytes / 1024.0)).append(" KB");
+ } else if (numBytes < 1024 * 1024 * 1024) {
+ DecimalFormat formatter = new DecimalFormat("#,##0.0");
+ buf.append(formatter.format((double) numBytes / (1024.0 * 1024.0))).append(" MB");
+ } else {
+ DecimalFormat formatter = new DecimalFormat("#,##0.0");
+ buf.append(formatter.format((double) numBytes / (1024.0 * 1024.0 * 1024.0)))
+ .append(" GB");
+ }
+ }
+ return buf;
+ }
+
+ public Object parseObject(String source, ParsePosition pos) {
+ return null;
+ }
+}
diff --git a/src/main/java/com/gitblit/utils/ClientLogger.java b/src/main/java/com/gitblit/utils/ClientLogger.java
new file mode 100644
index 00000000..7d18f3d6
--- /dev/null
+++ b/src/main/java/com/gitblit/utils/ClientLogger.java
@@ -0,0 +1,77 @@
+/*
+ * Copyright 2012 John Crygier
+ * Copyright 2012 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.PrintWriter;
+import java.io.StringWriter;
+
+import org.eclipse.jgit.transport.ReceivePack;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Class to log messages to the pushing Git client. Intended to be used by the
+ * Groovy Hooks.
+ *
+ * @author John Crygier
+ *
+ */
+public class ClientLogger {
+
+ static final Logger logger = LoggerFactory.getLogger(ClientLogger.class);
+ private ReceivePack rp;
+
+ public ClientLogger(ReceivePack rp) {
+ this.rp = rp;
+ }
+
+ /**
+ * Sends an info/warning message to the git client.
+ *
+ * @param message
+ */
+ public void info(String message) {
+ rp.sendMessage(message);
+ }
+
+ /**
+ * Sends an error message to the git client.
+ *
+ * @param message
+ */
+ public void error(String message) {
+ rp.sendError(message);
+ }
+
+ /**
+ * Sends an error message to the git client with an exception.
+ *
+ * @param message
+ * @param t
+ * an exception
+ */
+ public void error(String message, Throwable t) {
+ PrintWriter writer = new PrintWriter(new StringWriter());
+ if (!StringUtils.isEmpty(message)) {
+ writer.append(message);
+ writer.append('\n');
+ }
+ t.printStackTrace(writer);
+ rp.sendError(writer.toString());
+ }
+
+}
diff --git a/src/main/java/com/gitblit/utils/CompressionUtils.java b/src/main/java/com/gitblit/utils/CompressionUtils.java
new file mode 100644
index 00000000..a8dcdd8f
--- /dev/null
+++ b/src/main/java/com/gitblit/utils/CompressionUtils.java
@@ -0,0 +1,299 @@
+/*
+ * Copyright 2012 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.IOException;
+import java.io.OutputStream;
+import java.text.MessageFormat;
+import java.util.ArrayList;
+import java.util.List;
+
+import org.apache.commons.compress.archivers.tar.TarArchiveEntry;
+import org.apache.commons.compress.archivers.tar.TarArchiveOutputStream;
+import org.apache.commons.compress.archivers.zip.ZipArchiveEntry;
+import org.apache.commons.compress.archivers.zip.ZipArchiveOutputStream;
+import org.apache.commons.compress.compressors.CompressorException;
+import org.apache.commons.compress.compressors.CompressorStreamFactory;
+import org.apache.wicket.util.io.ByteArrayOutputStream;
+import org.eclipse.jgit.lib.Constants;
+import org.eclipse.jgit.lib.FileMode;
+import org.eclipse.jgit.lib.MutableObjectId;
+import org.eclipse.jgit.lib.ObjectLoader;
+import org.eclipse.jgit.lib.ObjectReader;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.revwalk.RevCommit;
+import org.eclipse.jgit.revwalk.RevWalk;
+import org.eclipse.jgit.treewalk.TreeWalk;
+import org.eclipse.jgit.treewalk.filter.PathFilter;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Collection of static methods for retrieving information from a repository.
+ *
+ * @author James Moger
+ *
+ */
+public class CompressionUtils {
+
+ static final Logger LOGGER = LoggerFactory.getLogger(CompressionUtils.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<Object> parameters = new ArrayList<Object>();
+ 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);
+ }
+
+ /**
+ * Zips the contents of the tree at the (optionally) specified revision and
+ * the (optionally) specified basepath to the supplied outputstream.
+ *
+ * @param repository
+ * @param basePath
+ * if unspecified, entire repository is assumed.
+ * @param objectId
+ * if unspecified, HEAD is assumed.
+ * @param os
+ * @return true if repository was successfully zipped to supplied output
+ * stream
+ */
+ public static boolean zip(Repository repository, String basePath, String objectId,
+ OutputStream os) {
+ RevCommit commit = JGitUtils.getCommit(repository, objectId);
+ if (commit == null) {
+ return false;
+ }
+ boolean success = false;
+ RevWalk rw = new RevWalk(repository);
+ TreeWalk tw = new TreeWalk(repository);
+ try {
+ tw.reset();
+ tw.addTree(commit.getTree());
+ ZipArchiveOutputStream zos = new ZipArchiveOutputStream(os);
+ zos.setComment("Generated by Gitblit");
+ if (!StringUtils.isEmpty(basePath)) {
+ PathFilter f = PathFilter.create(basePath);
+ tw.setFilter(f);
+ }
+ tw.setRecursive(true);
+ MutableObjectId id = new MutableObjectId();
+ ObjectReader reader = tw.getObjectReader();
+ long modified = commit.getAuthorIdent().getWhen().getTime();
+ while (tw.next()) {
+ FileMode mode = tw.getFileMode(0);
+ if (mode == FileMode.GITLINK || mode == FileMode.TREE) {
+ continue;
+ }
+ tw.getObjectId(id, 0);
+
+ ZipArchiveEntry entry = new ZipArchiveEntry(tw.getPathString());
+ entry.setSize(reader.getObjectSize(id, Constants.OBJ_BLOB));
+ entry.setComment(commit.getName());
+ entry.setUnixMode(mode.getBits());
+ entry.setTime(modified);
+ zos.putArchiveEntry(entry);
+
+ ObjectLoader ldr = repository.open(id);
+ ldr.copyTo(zos);
+ zos.closeArchiveEntry();
+ }
+ zos.finish();
+ success = true;
+ } catch (IOException e) {
+ error(e, repository, "{0} failed to zip files from commit {1}", commit.getName());
+ } finally {
+ tw.release();
+ rw.dispose();
+ }
+ return success;
+ }
+
+ /**
+ * tar the contents of the tree at the (optionally) specified revision and
+ * the (optionally) specified basepath to the supplied outputstream.
+ *
+ * @param repository
+ * @param basePath
+ * if unspecified, entire repository is assumed.
+ * @param objectId
+ * if unspecified, HEAD is assumed.
+ * @param os
+ * @return true if repository was successfully zipped to supplied output
+ * stream
+ */
+ public static boolean tar(Repository repository, String basePath, String objectId,
+ OutputStream os) {
+ return tar(null, repository, basePath, objectId, os);
+ }
+
+ /**
+ * tar.gz the contents of the tree at the (optionally) specified revision and
+ * the (optionally) specified basepath to the supplied outputstream.
+ *
+ * @param repository
+ * @param basePath
+ * if unspecified, entire repository is assumed.
+ * @param objectId
+ * if unspecified, HEAD is assumed.
+ * @param os
+ * @return true if repository was successfully zipped to supplied output
+ * stream
+ */
+ public static boolean gz(Repository repository, String basePath, String objectId,
+ OutputStream os) {
+ return tar(CompressorStreamFactory.GZIP, repository, basePath, objectId, os);
+ }
+
+ /**
+ * tar.xz the contents of the tree at the (optionally) specified revision and
+ * the (optionally) specified basepath to the supplied outputstream.
+ *
+ * @param repository
+ * @param basePath
+ * if unspecified, entire repository is assumed.
+ * @param objectId
+ * if unspecified, HEAD is assumed.
+ * @param os
+ * @return true if repository was successfully zipped to supplied output
+ * stream
+ */
+ public static boolean xz(Repository repository, String basePath, String objectId,
+ OutputStream os) {
+ return tar(CompressorStreamFactory.XZ, repository, basePath, objectId, os);
+ }
+
+ /**
+ * tar.bzip2 the contents of the tree at the (optionally) specified revision and
+ * the (optionally) specified basepath to the supplied outputstream.
+ *
+ * @param repository
+ * @param basePath
+ * if unspecified, entire repository is assumed.
+ * @param objectId
+ * if unspecified, HEAD is assumed.
+ * @param os
+ * @return true if repository was successfully zipped to supplied output
+ * stream
+ */
+ public static boolean bzip2(Repository repository, String basePath, String objectId,
+ OutputStream os) {
+
+ return tar(CompressorStreamFactory.BZIP2, repository, basePath, objectId, os);
+ }
+
+ /**
+ * Compresses/archives the contents of the tree at the (optionally)
+ * specified revision and the (optionally) specified basepath to the
+ * supplied outputstream.
+ *
+ * @param algorithm
+ * compression algorithm for tar (optional)
+ * @param repository
+ * @param basePath
+ * if unspecified, entire repository is assumed.
+ * @param objectId
+ * if unspecified, HEAD is assumed.
+ * @param os
+ * @return true if repository was successfully zipped to supplied output
+ * stream
+ */
+ private static boolean tar(String algorithm, Repository repository, String basePath, String objectId,
+ OutputStream os) {
+ RevCommit commit = JGitUtils.getCommit(repository, objectId);
+ if (commit == null) {
+ return false;
+ }
+
+ OutputStream cos = os;
+ if (!StringUtils.isEmpty(algorithm)) {
+ try {
+ cos = new CompressorStreamFactory().createCompressorOutputStream(algorithm, os);
+ } catch (CompressorException e1) {
+ error(e1, repository, "{0} failed to open {1} stream", algorithm);
+ }
+ }
+ boolean success = false;
+ RevWalk rw = new RevWalk(repository);
+ TreeWalk tw = new TreeWalk(repository);
+ try {
+ tw.reset();
+ tw.addTree(commit.getTree());
+ TarArchiveOutputStream tos = new TarArchiveOutputStream(cos);
+ tos.setAddPaxHeadersForNonAsciiNames(true);
+ tos.setLongFileMode(TarArchiveOutputStream.LONGFILE_POSIX);
+ if (!StringUtils.isEmpty(basePath)) {
+ PathFilter f = PathFilter.create(basePath);
+ tw.setFilter(f);
+ }
+ tw.setRecursive(true);
+ MutableObjectId id = new MutableObjectId();
+ long modified = commit.getAuthorIdent().getWhen().getTime();
+ while (tw.next()) {
+ FileMode mode = tw.getFileMode(0);
+ if (mode == FileMode.GITLINK || mode == FileMode.TREE) {
+ continue;
+ }
+ tw.getObjectId(id, 0);
+
+ ObjectLoader loader = repository.open(id);
+ if (FileMode.SYMLINK == mode) {
+ TarArchiveEntry entry = new TarArchiveEntry(tw.getPathString(),TarArchiveEntry.LF_SYMLINK);
+ ByteArrayOutputStream bos = new ByteArrayOutputStream();
+ loader.copyTo(bos);
+ entry.setLinkName(bos.toString());
+ entry.setModTime(modified);
+ tos.putArchiveEntry(entry);
+ tos.closeArchiveEntry();
+ } else {
+ TarArchiveEntry entry = new TarArchiveEntry(tw.getPathString());
+ entry.setMode(mode.getBits());
+ entry.setModTime(modified);
+ entry.setSize(loader.getSize());
+ tos.putArchiveEntry(entry);
+ loader.copyTo(tos);
+ tos.closeArchiveEntry();
+ }
+ }
+ tos.finish();
+ tos.close();
+ cos.close();
+ success = true;
+ } catch (IOException e) {
+ error(e, repository, "{0} failed to {1} stream files from commit {2}", algorithm, commit.getName());
+ } finally {
+ tw.release();
+ rw.dispose();
+ }
+ return success;
+ }
+}
diff --git a/src/main/java/com/gitblit/utils/ConnectionUtils.java b/src/main/java/com/gitblit/utils/ConnectionUtils.java
new file mode 100644
index 00000000..f0b41118
--- /dev/null
+++ b/src/main/java/com/gitblit/utils/ConnectionUtils.java
@@ -0,0 +1,214 @@
+/*
+ * 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.IOException;
+import java.net.InetAddress;
+import java.net.Socket;
+import java.net.URL;
+import java.net.URLConnection;
+import java.net.UnknownHostException;
+import java.security.GeneralSecurityException;
+import java.security.SecureRandom;
+import java.security.cert.CertificateException;
+import java.security.cert.X509Certificate;
+
+import javax.net.SocketFactory;
+import javax.net.ssl.HostnameVerifier;
+import javax.net.ssl.HttpsURLConnection;
+import javax.net.ssl.SSLContext;
+import javax.net.ssl.SSLSession;
+import javax.net.ssl.SSLSocketFactory;
+import javax.net.ssl.TrustManager;
+import javax.net.ssl.X509TrustManager;
+
+
+/**
+ * Utility class for establishing HTTP/HTTPS connections.
+ *
+ * @author James Moger
+ *
+ */
+public class ConnectionUtils {
+
+ static final String CHARSET;
+
+ private static final SSLContext SSL_CONTEXT;
+
+ private static final DummyHostnameVerifier HOSTNAME_VERIFIER;
+
+ static {
+ SSLContext context = null;
+ try {
+ context = SSLContext.getInstance("SSL");
+ context.init(null, new TrustManager[] { new DummyTrustManager() }, new SecureRandom());
+ } catch (Throwable t) {
+ t.printStackTrace();
+ }
+ SSL_CONTEXT = context;
+ HOSTNAME_VERIFIER = new DummyHostnameVerifier();
+ CHARSET = "UTF-8";
+ }
+
+ public static void setAuthorization(URLConnection conn, String username, char[] password) {
+ if (!StringUtils.isEmpty(username) && (password != null && password.length > 0)) {
+ conn.setRequestProperty(
+ "Authorization",
+ "Basic "
+ + Base64.encodeBytes((username + ":" + new String(password)).getBytes()));
+ }
+ }
+
+ public static URLConnection openReadConnection(String url, String username, char[] password)
+ throws IOException {
+ URLConnection conn = openConnection(url, username, password);
+ conn.setRequestProperty("Accept-Charset", ConnectionUtils.CHARSET);
+ return conn;
+ }
+
+ public static URLConnection openConnection(String url, String username, char[] password)
+ throws IOException {
+ URL urlObject = new URL(url);
+ URLConnection conn = urlObject.openConnection();
+ setAuthorization(conn, username, password);
+ conn.setUseCaches(false);
+ conn.setDoOutput(true);
+ if (conn instanceof HttpsURLConnection) {
+ HttpsURLConnection secureConn = (HttpsURLConnection) conn;
+ secureConn.setSSLSocketFactory(SSL_CONTEXT.getSocketFactory());
+ secureConn.setHostnameVerifier(HOSTNAME_VERIFIER);
+ }
+ return conn;
+ }
+
+ // Copyright (C) 2009 The Android Open Source Project
+ //
+ // 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.
+ public static class BlindSSLSocketFactory extends SSLSocketFactory {
+ private static final BlindSSLSocketFactory INSTANCE;
+
+ static {
+ try {
+ final SSLContext context = SSLContext.getInstance("SSL");
+ final TrustManager[] trustManagers = { new DummyTrustManager() };
+ final SecureRandom rng = new SecureRandom();
+ context.init(null, trustManagers, rng);
+ INSTANCE = new BlindSSLSocketFactory(context.getSocketFactory());
+ } catch (GeneralSecurityException e) {
+ throw new RuntimeException("Cannot create BlindSslSocketFactory", e);
+ }
+ }
+
+ public static SocketFactory getDefault() {
+ return INSTANCE;
+ }
+
+ private final SSLSocketFactory sslFactory;
+
+ private BlindSSLSocketFactory(final SSLSocketFactory sslFactory) {
+ this.sslFactory = sslFactory;
+ }
+
+ @Override
+ public Socket createSocket(Socket s, String host, int port, boolean autoClose)
+ throws IOException {
+ return sslFactory.createSocket(s, host, port, autoClose);
+ }
+
+ @Override
+ public String[] getDefaultCipherSuites() {
+ return sslFactory.getDefaultCipherSuites();
+ }
+
+ @Override
+ public String[] getSupportedCipherSuites() {
+ return sslFactory.getSupportedCipherSuites();
+ }
+
+ @Override
+ public Socket createSocket() throws IOException {
+ return sslFactory.createSocket();
+ }
+
+ @Override
+ public Socket createSocket(String host, int port) throws IOException,
+ UnknownHostException {
+ return sslFactory.createSocket(host, port);
+ }
+
+ @Override
+ public Socket createSocket(InetAddress host, int port) throws IOException {
+ return sslFactory.createSocket(host, port);
+ }
+
+ @Override
+ public Socket createSocket(String host, int port, InetAddress localHost,
+ int localPort) throws IOException, UnknownHostException {
+ return sslFactory.createSocket(host, port, localHost, localPort);
+ }
+
+ @Override
+ public Socket createSocket(InetAddress address, int port,
+ InetAddress localAddress, int localPort) throws IOException {
+ return sslFactory.createSocket(address, port, localAddress, localPort);
+ }
+ }
+
+ /**
+ * DummyTrustManager trusts all certificates.
+ *
+ * @author James Moger
+ */
+ private static class DummyTrustManager implements X509TrustManager {
+
+ @Override
+ public void checkClientTrusted(X509Certificate[] certs, String authType)
+ throws CertificateException {
+ }
+
+ @Override
+ public void checkServerTrusted(X509Certificate[] certs, String authType)
+ throws CertificateException {
+ }
+
+ @Override
+ public X509Certificate[] getAcceptedIssuers() {
+ return null;
+ }
+ }
+
+ /**
+ * Trusts all hostnames from a certificate, including self-signed certs.
+ *
+ * @author James Moger
+ */
+ private static class DummyHostnameVerifier implements HostnameVerifier {
+ @Override
+ public boolean verify(String hostname, SSLSession session) {
+ return true;
+ }
+ }
+}
diff --git a/src/main/java/com/gitblit/utils/ContainerUtils.java b/src/main/java/com/gitblit/utils/ContainerUtils.java
new file mode 100644
index 00000000..919f99d6
--- /dev/null
+++ b/src/main/java/com/gitblit/utils/ContainerUtils.java
@@ -0,0 +1,135 @@
+/*
+ * Copyright 2012 PD Inc / 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.CharConversionException;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.gitblit.GitBlit;
+import com.gitblit.Keys;
+
+/**
+ * This is the support class for all container specific code.
+ *
+ * @author jpyeron
+ */
+public class ContainerUtils
+{
+ private static Logger LOGGER = LoggerFactory.getLogger(ContainerUtils.class);
+
+ /**
+ * The support class for managing and evaluating the environment with
+ * regards to CVE-2007-0405.
+ *
+ * @see http://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2007-0450
+ * @author jpyeron
+ */
+ public static class CVE_2007_0450
+ {
+ /**
+ * This method will test for know issues in certain containers where %2F
+ * is blocked from use in URLs. It will emit a warning to the logger if
+ * the configuration of Tomcat causes the URL processing to fail on %2F.
+ */
+ public static void test()
+ {
+ if (GitBlit.getBoolean(Keys.web.mountParameters, true)
+ && ((GitBlit.getChar(Keys.web.forwardSlashCharacter, '/')) == '/' || (GitBlit.getChar(
+ Keys.web.forwardSlashCharacter, '/')) == '\\'))
+ {
+ try
+ {
+ if (GitBlit.isGO())
+ ;
+ else if (logCVE_2007_0450Tomcat())
+ ;
+ // else if (logCVE_2007_0450xxx());
+ else
+ {
+ LOGGER.info("Unknown container, cannot check for CVE-2007-0450 aplicability");
+ }
+ }
+ catch (Throwable t)
+ {
+ LOGGER.warn("Failure in checking for CVE-2007-0450 aplicability", t);
+ }
+ }
+
+ }
+
+ /**
+ * This method will test for know issues in certain versions of Tomcat,
+ * JBOSS, glassfish, and other embedded uses of Tomcat where %2F is
+ * blocked from use in certain URL s. It will emit a warning to the
+ * logger if the configuration of Tomcat causes the URL processing to
+ * fail on %2F.
+ *
+ * @return true if it recognizes Tomcat, false if it does not recognize
+ * Tomcat
+ */
+ private static boolean logCVE_2007_0450Tomcat()
+ {
+ try
+ {
+ byte[] test = "http://server.domain:8080/context/servlet/param%2fparam".getBytes();
+
+ // ByteChunk mb=new ByteChunk();
+ Class<?> cByteChunk = Class.forName("org.apache.tomcat.util.buf.ByteChunk");
+ Object mb = cByteChunk.newInstance();
+
+ // mb.setBytes(test, 0, test.length);
+ Method mByteChunck_setBytes = cByteChunk.getMethod("setBytes", byte[].class, int.class, int.class);
+ mByteChunck_setBytes.invoke(mb, test, (int) 0, test.length);
+
+ // UDecoder ud=new UDecoder();
+ Class<?> cUDecoder = Class.forName("org.apache.tomcat.util.buf.UDecoder");
+ Object ud = cUDecoder.newInstance();
+
+ // ud.convert(mb,false);
+ Method mUDecoder_convert = cUDecoder.getMethod("convert", cByteChunk, boolean.class);
+
+ try
+ {
+ mUDecoder_convert.invoke(ud, mb, false);
+ }
+ catch (InvocationTargetException e)
+ {
+ if (e.getTargetException() != null && e.getTargetException() instanceof CharConversionException)
+ {
+ LOGGER.warn("You are using a Tomcat-based server and your current settings will prevent grouped repositories, forks, personal repositories, and tree navigation from working properly. Please review the FAQ for details about running Gitblit on Tomcat. http://gitblit.com/faq.html and http://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2007-0450");
+ return true;
+ }
+ throw e;
+ }
+ }
+ catch (Throwable t)
+ {
+ // The apache url decoder internals are different, this is not a
+ // Tomcat matching the failure pattern for CVE-2007-0450
+ if (t instanceof ClassNotFoundException || t instanceof NoSuchMethodException
+ || t instanceof IllegalArgumentException)
+ return false;
+ LOGGER.debug("This is a tomcat, but the test operation failed somehow", t);
+ }
+ return true;
+ }
+ }
+
+}
diff --git a/src/main/java/com/gitblit/utils/DeepCopier.java b/src/main/java/com/gitblit/utils/DeepCopier.java
new file mode 100644
index 00000000..5df30623
--- /dev/null
+++ b/src/main/java/com/gitblit/utils/DeepCopier.java
@@ -0,0 +1,135 @@
+/*
+ * 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.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.ObjectInputStream;
+import java.io.ObjectOutputStream;
+import java.io.PipedInputStream;
+import java.io.PipedOutputStream;
+
+public class DeepCopier {
+
+ /**
+ * Produce a deep copy of the given object. Serializes the entire object to
+ * a byte array in memory. Recommended for relatively small objects.
+ */
+ @SuppressWarnings("unchecked")
+ public static <T> T copy(T original) {
+ T o = null;
+ try {
+ ByteArrayOutputStream byteOut = new ByteArrayOutputStream();
+ ObjectOutputStream oos = new ObjectOutputStream(byteOut);
+ oos.writeObject(original);
+ ByteArrayInputStream byteIn = new ByteArrayInputStream(byteOut.toByteArray());
+ ObjectInputStream ois = new ObjectInputStream(byteIn);
+ try {
+ o = (T) ois.readObject();
+ } catch (ClassNotFoundException cex) {
+ // actually can not happen in this instance
+ }
+ } catch (IOException iox) {
+ // doesn't seem likely to happen as these streams are in memory
+ throw new RuntimeException(iox);
+ }
+ return o;
+ }
+
+ /**
+ * This conserves heap memory!!!!! Produce a deep copy of the given object.
+ * Serializes the object through a pipe between two threads. Recommended for
+ * very large objects. The current thread is used for serializing the
+ * original object in order to respect any synchronization the caller may
+ * have around it, and a new thread is used for deserializing the copy.
+ *
+ */
+ public static <T> T copyParallel(T original) {
+ try {
+ PipedOutputStream outputStream = new PipedOutputStream();
+ PipedInputStream inputStream = new PipedInputStream(outputStream);
+ ObjectOutputStream ois = new ObjectOutputStream(outputStream);
+ Receiver<T> receiver = new Receiver<T>(inputStream);
+ try {
+ ois.writeObject(original);
+ } finally {
+ ois.close();
+ }
+ return receiver.getResult();
+ } catch (IOException iox) {
+ // doesn't seem likely to happen as these streams are in memory
+ throw new RuntimeException(iox);
+ }
+ }
+
+ private static class Receiver<T> extends Thread {
+
+ private final InputStream inputStream;
+ private volatile T result;
+ private volatile Throwable throwable;
+
+ public Receiver(InputStream inputStream) {
+ this.inputStream = inputStream;
+ start();
+ }
+
+ @SuppressWarnings("unchecked")
+ public void run() {
+
+ try {
+ ObjectInputStream ois = new ObjectInputStream(inputStream);
+ try {
+ result = (T) ois.readObject();
+ try {
+ // Some serializers may write more than they actually
+ // need to deserialize the object, but if we don't
+ // read it all the PipedOutputStream will choke.
+ while (inputStream.read() != -1) {
+ }
+ } catch (IOException e) {
+ // The object has been successfully deserialized, so
+ // ignore problems at this point (for example, the
+ // serializer may have explicitly closed the inputStream
+ // itself, causing this read to fail).
+ }
+ } finally {
+ ois.close();
+ }
+ } catch (Throwable t) {
+ throwable = t;
+ }
+ }
+
+ public T getResult() throws IOException {
+ try {
+ join();
+ } catch (InterruptedException e) {
+ throw new RuntimeException("Unexpected InterruptedException", e);
+ }
+ // join() guarantees that all shared memory is synchronized between
+ // the two threads
+ if (throwable != null) {
+ if (throwable instanceof ClassNotFoundException) {
+ // actually can not happen in this instance
+ }
+ throw new RuntimeException(throwable);
+ }
+ return result;
+ }
+ }
+} \ No newline at end of file
diff --git a/src/main/java/com/gitblit/utils/DiffUtils.java b/src/main/java/com/gitblit/utils/DiffUtils.java
new file mode 100644
index 00000000..04b5b0b1
--- /dev/null
+++ b/src/main/java/com/gitblit/utils/DiffUtils.java
@@ -0,0 +1,281 @@
+/*
+ * 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.ByteArrayOutputStream;
+import java.util.ArrayList;
+import java.util.List;
+
+import org.eclipse.jgit.api.BlameCommand;
+import org.eclipse.jgit.blame.BlameResult;
+import org.eclipse.jgit.diff.DiffEntry;
+import org.eclipse.jgit.diff.DiffFormatter;
+import org.eclipse.jgit.diff.RawText;
+import org.eclipse.jgit.diff.RawTextComparator;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.revwalk.RevCommit;
+import org.eclipse.jgit.revwalk.RevTree;
+import org.eclipse.jgit.revwalk.RevWalk;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.gitblit.models.AnnotatedLine;
+
+/**
+ * DiffUtils is a class of utility methods related to diff, patch, and blame.
+ *
+ * The diff methods support pluggable diff output types like Gitblit, Gitweb,
+ * and Plain.
+ *
+ * @author James Moger
+ *
+ */
+public class DiffUtils {
+
+ private static final Logger LOGGER = LoggerFactory.getLogger(DiffUtils.class);
+
+ /**
+ * Enumeration for the diff output types.
+ */
+ public static enum DiffOutputType {
+ PLAIN, GITWEB, GITBLIT;
+
+ public static DiffOutputType forName(String name) {
+ for (DiffOutputType type : values()) {
+ if (type.name().equalsIgnoreCase(name)) {
+ return type;
+ }
+ }
+ return null;
+ }
+ }
+
+ /**
+ * Returns the complete diff of the specified commit compared to its primary
+ * parent.
+ *
+ * @param repository
+ * @param commit
+ * @param outputType
+ * @return the diff as a string
+ */
+ public static String getCommitDiff(Repository repository, RevCommit commit,
+ DiffOutputType outputType) {
+ return getDiff(repository, null, commit, null, outputType);
+ }
+
+ /**
+ * Returns the diff for the specified file or folder from the specified
+ * commit compared to its primary parent.
+ *
+ * @param repository
+ * @param commit
+ * @param path
+ * @param outputType
+ * @return the diff as a string
+ */
+ public static String getDiff(Repository repository, RevCommit commit, String path,
+ DiffOutputType outputType) {
+ return getDiff(repository, null, commit, path, outputType);
+ }
+
+ /**
+ * Returns the complete diff between the two specified commits.
+ *
+ * @param repository
+ * @param baseCommit
+ * @param commit
+ * @param outputType
+ * @return the diff as a string
+ */
+ public static String getDiff(Repository repository, RevCommit baseCommit, RevCommit commit,
+ DiffOutputType outputType) {
+ return getDiff(repository, baseCommit, commit, null, outputType);
+ }
+
+ /**
+ * Returns the diff between two commits for the specified file.
+ *
+ * @param repository
+ * @param baseCommit
+ * if base commit is null the diff is to the primary parent of
+ * the commit.
+ * @param commit
+ * @param path
+ * if the path is specified, the diff is restricted to that file
+ * or folder. if unspecified, the diff is for the entire commit.
+ * @param outputType
+ * @return the diff as a string
+ */
+ public static String getDiff(Repository repository, RevCommit baseCommit, RevCommit commit,
+ String path, DiffOutputType outputType) {
+ String diff = null;
+ try {
+ final ByteArrayOutputStream os = new ByteArrayOutputStream();
+ RawTextComparator cmp = RawTextComparator.DEFAULT;
+ DiffFormatter df;
+ switch (outputType) {
+ case GITWEB:
+ df = new GitWebDiffFormatter(os);
+ break;
+ case GITBLIT:
+ df = new GitBlitDiffFormatter(os);
+ break;
+ case PLAIN:
+ default:
+ df = new DiffFormatter(os);
+ break;
+ }
+ df.setRepository(repository);
+ df.setDiffComparator(cmp);
+ df.setDetectRenames(true);
+
+ RevTree commitTree = commit.getTree();
+ RevTree baseTree;
+ if (baseCommit == null) {
+ if (commit.getParentCount() > 0) {
+ final RevWalk rw = new RevWalk(repository);
+ RevCommit parent = rw.parseCommit(commit.getParent(0).getId());
+ rw.dispose();
+ baseTree = parent.getTree();
+ } else {
+ // FIXME initial commit. no parent?!
+ baseTree = commitTree;
+ }
+ } else {
+ baseTree = baseCommit.getTree();
+ }
+
+ List<DiffEntry> diffEntries = df.scan(baseTree, commitTree);
+ if (path != null && path.length() > 0) {
+ for (DiffEntry diffEntry : diffEntries) {
+ if (diffEntry.getNewPath().equalsIgnoreCase(path)) {
+ df.format(diffEntry);
+ break;
+ }
+ }
+ } else {
+ df.format(diffEntries);
+ }
+ if (df instanceof GitWebDiffFormatter) {
+ // workaround for complex private methods in DiffFormatter
+ diff = ((GitWebDiffFormatter) df).getHtml();
+ } else {
+ diff = os.toString();
+ }
+ df.flush();
+ } catch (Throwable t) {
+ LOGGER.error("failed to generate commit diff!", t);
+ }
+ return diff;
+ }
+
+ /**
+ * Returns the diff between the two commits for the specified file or folder
+ * formatted as a patch.
+ *
+ * @param repository
+ * @param baseCommit
+ * if base commit is unspecified, the patch is generated against
+ * the primary parent of the specified commit.
+ * @param commit
+ * @param path
+ * if path is specified, the patch is generated only for the
+ * specified file or folder. if unspecified, the patch is
+ * generated for the entire diff between the two commits.
+ * @return patch as a string
+ */
+ public static String getCommitPatch(Repository repository, RevCommit baseCommit,
+ RevCommit commit, String path) {
+ String diff = null;
+ try {
+ final ByteArrayOutputStream os = new ByteArrayOutputStream();
+ RawTextComparator cmp = RawTextComparator.DEFAULT;
+ PatchFormatter df = new PatchFormatter(os);
+ df.setRepository(repository);
+ df.setDiffComparator(cmp);
+ df.setDetectRenames(true);
+
+ RevTree commitTree = commit.getTree();
+ RevTree baseTree;
+ if (baseCommit == null) {
+ if (commit.getParentCount() > 0) {
+ final RevWalk rw = new RevWalk(repository);
+ RevCommit parent = rw.parseCommit(commit.getParent(0).getId());
+ baseTree = parent.getTree();
+ } else {
+ // FIXME initial commit. no parent?!
+ baseTree = commitTree;
+ }
+ } else {
+ baseTree = baseCommit.getTree();
+ }
+
+ List<DiffEntry> diffEntries = df.scan(baseTree, commitTree);
+ if (path != null && path.length() > 0) {
+ for (DiffEntry diffEntry : diffEntries) {
+ if (diffEntry.getNewPath().equalsIgnoreCase(path)) {
+ df.format(diffEntry);
+ break;
+ }
+ }
+ } else {
+ df.format(diffEntries);
+ }
+ diff = df.getPatch(commit);
+ df.flush();
+ } catch (Throwable t) {
+ LOGGER.error("failed to generate commit diff!", t);
+ }
+ return diff;
+ }
+
+ /**
+ * Returns the list of lines in the specified source file annotated with the
+ * source commit metadata.
+ *
+ * @param repository
+ * @param blobPath
+ * @param objectId
+ * @return list of annotated lines
+ */
+ public static List<AnnotatedLine> blame(Repository repository, String blobPath, String objectId) {
+ List<AnnotatedLine> lines = new ArrayList<AnnotatedLine>();
+ try {
+ ObjectId object;
+ if (StringUtils.isEmpty(objectId)) {
+ object = JGitUtils.getDefaultBranch(repository);
+ } else {
+ object = repository.resolve(objectId);
+ }
+ BlameCommand blameCommand = new BlameCommand(repository);
+ blameCommand.setFilePath(blobPath);
+ blameCommand.setStartCommit(object);
+ BlameResult blameResult = blameCommand.call();
+ RawText rawText = blameResult.getResultContents();
+ int length = rawText.size();
+ for (int i = 0; i < length; i++) {
+ RevCommit commit = blameResult.getSourceCommit(i);
+ AnnotatedLine line = new AnnotatedLine(commit, i + 1, rawText.getString(i));
+ lines.add(line);
+ }
+ } catch (Throwable t) {
+ LOGGER.error("failed to generate blame!", t);
+ }
+ return lines;
+ }
+}
diff --git a/src/main/java/com/gitblit/utils/FederationUtils.java b/src/main/java/com/gitblit/utils/FederationUtils.java
new file mode 100644
index 00000000..4d6060dd
--- /dev/null
+++ b/src/main/java/com/gitblit/utils/FederationUtils.java
@@ -0,0 +1,349 @@
+/*
+ * 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.lang.reflect.Type;
+import java.text.MessageFormat;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import javax.servlet.http.HttpServletResponse;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.gitblit.Constants;
+import com.gitblit.Constants.FederationProposalResult;
+import com.gitblit.Constants.FederationRequest;
+import com.gitblit.Constants.FederationToken;
+import com.gitblit.IStoredSettings;
+import com.gitblit.Keys;
+import com.gitblit.models.FederationModel;
+import com.gitblit.models.FederationProposal;
+import com.gitblit.models.RepositoryModel;
+import com.gitblit.models.TeamModel;
+import com.gitblit.models.UserModel;
+import com.google.gson.reflect.TypeToken;
+
+/**
+ * Utility methods for federation functions.
+ *
+ * @author James Moger
+ *
+ */
+public class FederationUtils {
+
+ private static final Type REPOSITORIES_TYPE = new TypeToken<Map<String, RepositoryModel>>() {
+ }.getType();
+
+ private static final Type SETTINGS_TYPE = new TypeToken<Map<String, String>>() {
+ }.getType();
+
+ private static final Type USERS_TYPE = new TypeToken<Collection<UserModel>>() {
+ }.getType();
+
+ private static final Type TEAMS_TYPE = new TypeToken<Collection<TeamModel>>() {
+ }.getType();
+
+ private static final Logger LOGGER = LoggerFactory.getLogger(FederationUtils.class);
+
+ /**
+ * Returns an url to this servlet for the specified parameters.
+ *
+ * @param sourceURL
+ * the url of the source gitblit instance
+ * @param token
+ * the federation token of the source gitblit instance
+ * @param req
+ * the pull type request
+ */
+ public static String asLink(String sourceURL, String token, FederationRequest req) {
+ return asLink(sourceURL, null, token, req, null);
+ }
+
+ /**
+ *
+ * @param remoteURL
+ * the url of the remote gitblit instance
+ * @param tokenType
+ * the type of federation token of a gitblit instance
+ * @param token
+ * the federation token of a gitblit instance
+ * @param req
+ * the pull type request
+ * @param myURL
+ * the url of this gitblit instance
+ * @return
+ */
+ public static String asLink(String remoteURL, FederationToken tokenType, String token,
+ FederationRequest req, String myURL) {
+ if (remoteURL.length() > 0 && remoteURL.charAt(remoteURL.length() - 1) == '/') {
+ remoteURL = remoteURL.substring(0, remoteURL.length() - 1);
+ }
+ if (req == null) {
+ req = FederationRequest.PULL_REPOSITORIES;
+ }
+ return remoteURL + Constants.FEDERATION_PATH + "?req=" + req.name().toLowerCase()
+ + (token == null ? "" : ("&token=" + token))
+ + (tokenType == null ? "" : ("&tokenType=" + tokenType.name().toLowerCase()))
+ + (myURL == null ? "" : ("&url=" + StringUtils.encodeURL(myURL)));
+ }
+
+ /**
+ * Returns the list of federated gitblit instances that this instance will
+ * try to pull.
+ *
+ * @return list of registered gitblit instances
+ */
+ public static List<FederationModel> getFederationRegistrations(IStoredSettings settings) {
+ List<FederationModel> federationRegistrations = new ArrayList<FederationModel>();
+ List<String> keys = settings.getAllKeys(Keys.federation._ROOT);
+ keys.remove(Keys.federation.name);
+ keys.remove(Keys.federation.passphrase);
+ keys.remove(Keys.federation.allowProposals);
+ keys.remove(Keys.federation.proposalsFolder);
+ keys.remove(Keys.federation.defaultFrequency);
+ keys.remove(Keys.federation.sets);
+ Collections.sort(keys);
+ Map<String, FederationModel> federatedModels = new HashMap<String, FederationModel>();
+ for (String key : keys) {
+ String value = key.substring(Keys.federation._ROOT.length() + 1);
+ List<String> values = StringUtils.getStringsFromValue(value, "\\.");
+ String server = values.get(0);
+ if (!federatedModels.containsKey(server)) {
+ federatedModels.put(server, new FederationModel(server));
+ }
+ String setting = values.get(1);
+ if (setting.equals("url")) {
+ // url of the origin Gitblit instance
+ federatedModels.get(server).url = settings.getString(key, "");
+ } else if (setting.equals("token")) {
+ // token for the origin Gitblit instance
+ federatedModels.get(server).token = settings.getString(key, "");
+ } else if (setting.equals("frequency")) {
+ // frequency of the pull operation
+ federatedModels.get(server).frequency = settings.getString(key, "");
+ } else if (setting.equals("folder")) {
+ // destination folder of the pull operation
+ federatedModels.get(server).folder = settings.getString(key, "");
+ } else if (setting.equals("bare")) {
+ // whether pulled repositories should be bare
+ federatedModels.get(server).bare = settings.getBoolean(key, true);
+ } else if (setting.equals("mirror")) {
+ // are the repositories to be true mirrors of the origin
+ federatedModels.get(server).mirror = settings.getBoolean(key, true);
+ } else if (setting.equals("mergeAccounts")) {
+ // merge remote accounts into local accounts
+ federatedModels.get(server).mergeAccounts = settings.getBoolean(key, false);
+ } else if (setting.equals("sendStatus")) {
+ // send a status acknowledgment to source Gitblit instance
+ // at end of git pull
+ federatedModels.get(server).sendStatus = settings.getBoolean(key, false);
+ } else if (setting.equals("notifyOnError")) {
+ // notify administrators on federation pull failures
+ federatedModels.get(server).notifyOnError = settings.getBoolean(key, false);
+ } else if (setting.equals("exclude")) {
+ // excluded repositories
+ federatedModels.get(server).exclusions = settings.getStrings(key);
+ } else if (setting.equals("include")) {
+ // included repositories
+ federatedModels.get(server).inclusions = settings.getStrings(key);
+ }
+ }
+
+ // verify that registrations have a url and a token
+ for (FederationModel model : federatedModels.values()) {
+ if (StringUtils.isEmpty(model.url)) {
+ LOGGER.warn(MessageFormat.format(
+ "Dropping federation registration {0}. Missing url.", model.name));
+ continue;
+ }
+ if (StringUtils.isEmpty(model.token)) {
+ LOGGER.warn(MessageFormat.format(
+ "Dropping federation registration {0}. Missing token.", model.name));
+ continue;
+ }
+ // set default frequency if unspecified
+ if (StringUtils.isEmpty(model.frequency)) {
+ model.frequency = settings.getString(Keys.federation.defaultFrequency, "60 mins");
+ }
+ federationRegistrations.add(model);
+ }
+ return federationRegistrations;
+ }
+
+ /**
+ * Sends a federation poke to the Gitblit instance at remoteUrl. Pokes are
+ * sent by an pulling Gitblit instance to an origin Gitblit instance as part
+ * of the proposal process. This is to ensure that the pulling Gitblit
+ * instance has an IP route to the origin instance.
+ *
+ * @param remoteUrl
+ * the remote Gitblit instance to send a federation proposal to
+ * @param proposal
+ * a complete federation proposal
+ * @return true if there is a route to the remoteUrl
+ */
+ public static boolean poke(String remoteUrl) throws Exception {
+ String url = asLink(remoteUrl, null, FederationRequest.POKE);
+ String json = JsonUtils.toJsonString("POKE");
+ int status = JsonUtils.sendJsonString(url, json);
+ return status == HttpServletResponse.SC_OK;
+ }
+
+ /**
+ * Sends a federation proposal to the Gitblit instance at remoteUrl
+ *
+ * @param remoteUrl
+ * the remote Gitblit instance to send a federation proposal to
+ * @param proposal
+ * a complete federation proposal
+ * @return the federation proposal result code
+ */
+ public static FederationProposalResult propose(String remoteUrl, FederationProposal proposal)
+ throws Exception {
+ String url = asLink(remoteUrl, null, FederationRequest.PROPOSAL);
+ String json = JsonUtils.toJsonString(proposal);
+ int status = JsonUtils.sendJsonString(url, json);
+ switch (status) {
+ case HttpServletResponse.SC_FORBIDDEN:
+ // remote Gitblit Federation disabled
+ return FederationProposalResult.FEDERATION_DISABLED;
+ case HttpServletResponse.SC_BAD_REQUEST:
+ // remote Gitblit did not receive any JSON data
+ return FederationProposalResult.MISSING_DATA;
+ case HttpServletResponse.SC_METHOD_NOT_ALLOWED:
+ // remote Gitblit not accepting proposals
+ return FederationProposalResult.NO_PROPOSALS;
+ case HttpServletResponse.SC_NOT_ACCEPTABLE:
+ // remote Gitblit failed to poke this Gitblit instance
+ return FederationProposalResult.NO_POKE;
+ case HttpServletResponse.SC_OK:
+ // received
+ return FederationProposalResult.ACCEPTED;
+ default:
+ return FederationProposalResult.ERROR;
+ }
+ }
+
+ /**
+ * Retrieves a map of the repositories at the remote gitblit instance keyed
+ * by the repository clone url.
+ *
+ * @param registration
+ * @param checkExclusions
+ * should returned repositories remove registration exclusions
+ * @return a map of cloneable repositories
+ * @throws Exception
+ */
+ public static Map<String, RepositoryModel> getRepositories(FederationModel registration,
+ boolean checkExclusions) throws Exception {
+ String url = asLink(registration.url, registration.token,
+ FederationRequest.PULL_REPOSITORIES);
+ Map<String, RepositoryModel> models = JsonUtils.retrieveJson(url, REPOSITORIES_TYPE);
+ if (checkExclusions) {
+ Map<String, RepositoryModel> includedModels = new HashMap<String, RepositoryModel>();
+ for (Map.Entry<String, RepositoryModel> entry : models.entrySet()) {
+ if (registration.isIncluded(entry.getValue())) {
+ includedModels.put(entry.getKey(), entry.getValue());
+ }
+ }
+ return includedModels;
+ }
+ return models;
+ }
+
+ /**
+ * Tries to pull the gitblit user accounts from the remote gitblit instance.
+ *
+ * @param registration
+ * @return a collection of UserModel objects
+ * @throws Exception
+ */
+ public static List<UserModel> getUsers(FederationModel registration) throws Exception {
+ String url = asLink(registration.url, registration.token, FederationRequest.PULL_USERS);
+ Collection<UserModel> models = JsonUtils.retrieveJson(url, USERS_TYPE);
+ List<UserModel> list = new ArrayList<UserModel>(models);
+ return list;
+ }
+
+ /**
+ * Tries to pull the gitblit team definitions from the remote gitblit
+ * instance.
+ *
+ * @param registration
+ * @return a collection of TeamModel objects
+ * @throws Exception
+ */
+ public static List<TeamModel> getTeams(FederationModel registration) throws Exception {
+ String url = asLink(registration.url, registration.token, FederationRequest.PULL_TEAMS);
+ Collection<TeamModel> models = JsonUtils.retrieveJson(url, TEAMS_TYPE);
+ List<TeamModel> list = new ArrayList<TeamModel>(models);
+ return list;
+ }
+
+ /**
+ * Tries to pull the gitblit server settings from the remote gitblit
+ * instance.
+ *
+ * @param registration
+ * @return a map of the remote gitblit settings
+ * @throws Exception
+ */
+ public static Map<String, String> getSettings(FederationModel registration) throws Exception {
+ String url = asLink(registration.url, registration.token, FederationRequest.PULL_SETTINGS);
+ Map<String, String> settings = JsonUtils.retrieveJson(url, SETTINGS_TYPE);
+ return settings;
+ }
+
+ /**
+ * Tries to pull the referenced scripts from the remote gitblit instance.
+ *
+ * @param registration
+ * @return a map of the remote gitblit scripts by script name
+ * @throws Exception
+ */
+ public static Map<String, String> getScripts(FederationModel registration) throws Exception {
+ String url = asLink(registration.url, registration.token, FederationRequest.PULL_SCRIPTS);
+ Map<String, String> scripts = JsonUtils.retrieveJson(url, SETTINGS_TYPE);
+ return scripts;
+ }
+
+ /**
+ * Send an status acknowledgment to the remote Gitblit server.
+ *
+ * @param identification
+ * identification of this pulling instance
+ * @param registration
+ * the source Gitblit instance to receive an acknowledgment
+ * @param results
+ * the results of your pull operation
+ * @return true, if the remote Gitblit instance acknowledged your results
+ * @throws Exception
+ */
+ public static boolean acknowledgeStatus(String identification, FederationModel registration)
+ throws Exception {
+ String url = asLink(registration.url, null, registration.token, FederationRequest.STATUS,
+ identification);
+ String json = JsonUtils.toJsonString(registration);
+ int status = JsonUtils.sendJsonString(url, json);
+ return status == HttpServletResponse.SC_OK;
+ }
+}
diff --git a/src/main/java/com/gitblit/utils/FileUtils.java b/src/main/java/com/gitblit/utils/FileUtils.java
new file mode 100644
index 00000000..a21b5128
--- /dev/null
+++ b/src/main/java/com/gitblit/utils/FileUtils.java
@@ -0,0 +1,292 @@
+/*
+ * 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.BufferedInputStream;
+import java.io.BufferedReader;
+import java.io.BufferedWriter;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.io.OutputStreamWriter;
+import java.nio.charset.Charset;
+
+/**
+ * Common file utilities.
+ *
+ * @author James Moger
+ *
+ */
+public class FileUtils {
+
+ /** 1024 (number of bytes in one kilobyte) */
+ public static final int KB = 1024;
+
+ /** 1024 {@link #KB} (number of bytes in one megabyte) */
+ public static final int MB = 1024 * KB;
+
+ /** 1024 {@link #MB} (number of bytes in one gigabyte) */
+ public static final int GB = 1024 * MB;
+
+ /**
+ * Returns an int from a string representation of a file size.
+ * e.g. 50m = 50 megabytes
+ *
+ * @param aString
+ * @param defaultValue
+ * @return an int value or the defaultValue if aString can not be parsed
+ */
+ public static int convertSizeToInt(String aString, int defaultValue) {
+ return (int) convertSizeToLong(aString, defaultValue);
+ }
+
+ /**
+ * Returns a long from a string representation of a file size.
+ * e.g. 50m = 50 megabytes
+ *
+ * @param aString
+ * @param defaultValue
+ * @return a long value or the defaultValue if aString can not be parsed
+ */
+ public static long convertSizeToLong(String aString, long defaultValue) {
+ // trim string and remove all spaces
+ aString = aString.toLowerCase().trim();
+ StringBuilder sb = new StringBuilder();
+ for (String a : aString.split(" ")) {
+ sb.append(a);
+ }
+ aString = sb.toString();
+
+ // identify value and unit
+ int idx = 0;
+ int len = aString.length();
+ while (Character.isDigit(aString.charAt(idx))) {
+ idx++;
+ if (idx == len) {
+ break;
+ }
+ }
+ long value = 0;
+ String unit = null;
+ try {
+ value = Long.parseLong(aString.substring(0, idx));
+ unit = aString.substring(idx);
+ } catch (Exception e) {
+ return defaultValue;
+ }
+ if (unit.equals("g") || unit.equals("gb")) {
+ return value * GB;
+ } else if (unit.equals("m") || unit.equals("mb")) {
+ return value * MB;
+ } else if (unit.equals("k") || unit.equals("kb")) {
+ return value * KB;
+ }
+ return defaultValue;
+ }
+
+ /**
+ * Returns the byte [] content of the specified file.
+ *
+ * @param file
+ * @return the byte content of the file
+ */
+ public static byte [] readContent(File file) {
+ byte [] buffer = new byte[(int) file.length()];
+ try {
+ BufferedInputStream is = new BufferedInputStream(new FileInputStream(file));
+ is.read(buffer, 0, buffer.length);
+ is.close();
+ } catch (Throwable t) {
+ System.err.println("Failed to read byte content of " + file.getAbsolutePath());
+ t.printStackTrace();
+ }
+ return buffer;
+ }
+
+ /**
+ * Returns the string content of the specified file.
+ *
+ * @param file
+ * @param lineEnding
+ * @return the string content of the file
+ */
+ public static String readContent(File file, String lineEnding) {
+ StringBuilder sb = new StringBuilder();
+ try {
+ InputStreamReader is = new InputStreamReader(new FileInputStream(file),
+ Charset.forName("UTF-8"));
+ BufferedReader reader = new BufferedReader(is);
+ String line = null;
+ while ((line = reader.readLine()) != null) {
+ sb.append(line);
+ if (lineEnding != null) {
+ sb.append(lineEnding);
+ }
+ }
+ reader.close();
+ } catch (Throwable t) {
+ System.err.println("Failed to read content of " + file.getAbsolutePath());
+ t.printStackTrace();
+ }
+ return sb.toString();
+ }
+
+ /**
+ * Writes the string content to the file.
+ *
+ * @param file
+ * @param content
+ */
+ public static void writeContent(File file, String content) {
+ try {
+ OutputStreamWriter os = new OutputStreamWriter(new FileOutputStream(file),
+ Charset.forName("UTF-8"));
+ BufferedWriter writer = new BufferedWriter(os);
+ writer.append(content);
+ writer.close();
+ } catch (Throwable t) {
+ System.err.println("Failed to write content of " + file.getAbsolutePath());
+ t.printStackTrace();
+ }
+ }
+
+ /**
+ * Recursively traverses a folder and its subfolders to calculate the total
+ * size in bytes.
+ *
+ * @param directory
+ * @return folder size in bytes
+ */
+ public static long folderSize(File directory) {
+ if (directory == null || !directory.exists()) {
+ return -1;
+ }
+ if (directory.isDirectory()) {
+ long length = 0;
+ for (File file : directory.listFiles()) {
+ length += folderSize(file);
+ }
+ return length;
+ } else if (directory.isFile()) {
+ return directory.length();
+ }
+ return 0;
+ }
+
+ /**
+ * Copies a file or folder (recursively) to a destination folder.
+ *
+ * @param destinationFolder
+ * @param filesOrFolders
+ * @return
+ * @throws FileNotFoundException
+ * @throws IOException
+ */
+ public static void copy(File destinationFolder, File... filesOrFolders)
+ throws FileNotFoundException, IOException {
+ destinationFolder.mkdirs();
+ for (File file : filesOrFolders) {
+ if (file.isDirectory()) {
+ copy(new File(destinationFolder, file.getName()), file.listFiles());
+ } else {
+ File dFile = new File(destinationFolder, file.getName());
+ BufferedInputStream bufin = null;
+ FileOutputStream fos = null;
+ try {
+ bufin = new BufferedInputStream(new FileInputStream(file));
+ fos = new FileOutputStream(dFile);
+ int len = 8196;
+ byte[] buff = new byte[len];
+ int n = 0;
+ while ((n = bufin.read(buff, 0, len)) != -1) {
+ fos.write(buff, 0, n);
+ }
+ } finally {
+ try {
+ bufin.close();
+ } catch (Throwable t) {
+ }
+ try {
+ fos.close();
+ } catch (Throwable t) {
+ }
+ }
+ dFile.setLastModified(file.lastModified());
+ }
+ }
+ }
+
+ /**
+ * Determine the relative path between two files. Takes into account
+ * canonical paths, if possible.
+ *
+ * @param basePath
+ * @param path
+ * @return a relative path from basePath to path
+ */
+ public static String getRelativePath(File basePath, File path) {
+ File exactBase = getExactFile(basePath);
+ File exactPath = getExactFile(path);
+ if (path.getAbsolutePath().startsWith(basePath.getAbsolutePath())) {
+ // absolute base-path match
+ return StringUtils.getRelativePath(basePath.getAbsolutePath(), path.getAbsolutePath());
+ } else if (exactPath.getPath().startsWith(exactBase.getPath())) {
+ // canonical base-path match
+ return StringUtils.getRelativePath(exactBase.getPath(), exactPath.getPath());
+ } else if (exactPath.getPath().startsWith(basePath.getAbsolutePath())) {
+ // mixed path match
+ return StringUtils.getRelativePath(basePath.getAbsolutePath(), exactPath.getPath());
+ } else if (path.getAbsolutePath().startsWith(exactBase.getPath())) {
+ // mixed path match
+ return StringUtils.getRelativePath(exactBase.getPath(), path.getAbsolutePath());
+ }
+ // no relative relationship
+ return null;
+ }
+
+ /**
+ * Returns the exact path for a file. This path will be the canonical path
+ * unless an exception is thrown in which case it will be the absolute path.
+ *
+ * @param path
+ * @return the exact file
+ */
+ public static File getExactFile(File path) {
+ try {
+ return path.getCanonicalFile();
+ } catch (IOException e) {
+ return path.getAbsoluteFile();
+ }
+ }
+
+ public static File resolveParameter(String parameter, File aFolder, String path) {
+ if (aFolder == null) {
+ // strip any parameter reference
+ path = path.replace(parameter, "").trim();
+ if (path.length() > 0 && path.charAt(0) == '/') {
+ // strip leading /
+ path = path.substring(1);
+ }
+ } else if (path.contains(parameter)) {
+ // replace parameter with path
+ path = path.replace(parameter, aFolder.getAbsolutePath());
+ }
+ return new File(path);
+ }
+}
diff --git a/src/main/java/com/gitblit/utils/GitBlitDiffFormatter.java b/src/main/java/com/gitblit/utils/GitBlitDiffFormatter.java
new file mode 100644
index 00000000..2966aa8a
--- /dev/null
+++ b/src/main/java/com/gitblit/utils/GitBlitDiffFormatter.java
@@ -0,0 +1,164 @@
+/*
+ * 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 static org.eclipse.jgit.lib.Constants.encode;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.OutputStream;
+
+import org.eclipse.jgit.diff.RawText;
+import org.eclipse.jgit.util.RawParseUtils;
+
+/**
+ * Generates an html snippet of a diff in Gitblit's style.
+ *
+ * @author James Moger
+ *
+ */
+public class GitBlitDiffFormatter extends GitWebDiffFormatter {
+
+ private final OutputStream os;
+
+ private int left, right;
+
+ public GitBlitDiffFormatter(OutputStream os) {
+ super(os);
+ this.os = os;
+ }
+
+ /**
+ * Output a hunk header
+ *
+ * @param aStartLine
+ * within first source
+ * @param aEndLine
+ * within first source
+ * @param bStartLine
+ * within second source
+ * @param bEndLine
+ * within second source
+ * @throws IOException
+ */
+ @Override
+ protected void writeHunkHeader(int aStartLine, int aEndLine, int bStartLine, int bEndLine)
+ throws IOException {
+ os.write("<tr><th>..</th><th>..</th><td class='hunk_header'>".getBytes());
+ os.write('@');
+ os.write('@');
+ writeRange('-', aStartLine + 1, aEndLine - aStartLine);
+ writeRange('+', bStartLine + 1, bEndLine - bStartLine);
+ os.write(' ');
+ os.write('@');
+ os.write('@');
+ os.write("</td></tr>\n".getBytes());
+ left = aStartLine + 1;
+ right = bStartLine + 1;
+ }
+
+ @Override
+ protected void writeLine(final char prefix, final RawText text, final int cur)
+ throws IOException {
+ os.write("<tr>".getBytes());
+ switch (prefix) {
+ case '+':
+ os.write(("<th></th><th>" + (right++) + "</th>").getBytes());
+ os.write("<td><div class=\"diff add2\">".getBytes());
+ break;
+ case '-':
+ os.write(("<th>" + (left++) + "</th><th></th>").getBytes());
+ os.write("<td><div class=\"diff remove2\">".getBytes());
+ break;
+ default:
+ os.write(("<th>" + (left++) + "</th><th>" + (right++) + "</th>").getBytes());
+ os.write("<td>".getBytes());
+ break;
+ }
+ os.write(prefix);
+ String line = text.getString(cur);
+ line = StringUtils.escapeForHtml(line, false);
+ os.write(encode(line));
+ switch (prefix) {
+ case '+':
+ case '-':
+ os.write("</div>".getBytes());
+ break;
+ default:
+ os.write("</td>".getBytes());
+ }
+ os.write("</tr>\n".getBytes());
+ }
+
+ /**
+ * Workaround function for complex private methods in DiffFormatter. This
+ * sets the html for the diff headers.
+ *
+ * @return
+ */
+ @Override
+ public String getHtml() {
+ ByteArrayOutputStream bos = (ByteArrayOutputStream) os;
+ String html = RawParseUtils.decode(bos.toByteArray());
+ String[] lines = html.split("\n");
+ StringBuilder sb = new StringBuilder();
+ boolean inFile = false;
+ String oldnull = "a/dev/null";
+ for (String line : lines) {
+ if (line.startsWith("index")) {
+ // skip index lines
+ } else if (line.startsWith("new file")) {
+ // skip new file lines
+ } else if (line.startsWith("\\ No newline")) {
+ // skip no new line
+ } else if (line.startsWith("---") || line.startsWith("+++")) {
+ // skip --- +++ lines
+ } else if (line.startsWith("diff")) {
+ line = StringUtils.convertOctal(line);
+ if (line.indexOf(oldnull) > -1) {
+ // a is null, use b
+ line = line.substring(("diff --git " + oldnull).length()).trim();
+ // trim b/
+ line = line.substring(2).trim();
+ } else {
+ // use a
+ line = line.substring("diff --git ".length()).trim();
+ line = line.substring(line.startsWith("\"a/") ? 3 : 2);
+ line = line.substring(0, line.indexOf(" b/") > -1 ? line.indexOf(" b/") : line.indexOf("\"b/")).trim();
+ }
+
+ if (line.charAt(0) == '"') {
+ line = line.substring(1);
+ }
+ if (line.charAt(line.length() - 1) == '"') {
+ line = line.substring(0, line.length() - 1);
+ }
+ if (inFile) {
+ sb.append("</tbody></table></div>\n");
+ inFile = false;
+ }
+ sb.append("<div class='header'>").append(line).append("</div>");
+ sb.append("<div class=\"diff\">");
+ sb.append("<table><tbody>");
+ inFile = true;
+ } else {
+ sb.append(line);
+ }
+ }
+ sb.append("</table></div>");
+ return sb.toString();
+ }
+}
diff --git a/src/main/java/com/gitblit/utils/GitWebDiffFormatter.java b/src/main/java/com/gitblit/utils/GitWebDiffFormatter.java
new file mode 100644
index 00000000..e657dc58
--- /dev/null
+++ b/src/main/java/com/gitblit/utils/GitWebDiffFormatter.java
@@ -0,0 +1,155 @@
+/*
+ * 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 static org.eclipse.jgit.lib.Constants.encode;
+import static org.eclipse.jgit.lib.Constants.encodeASCII;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.OutputStream;
+
+import org.eclipse.jgit.diff.DiffFormatter;
+import org.eclipse.jgit.diff.RawText;
+import org.eclipse.jgit.util.RawParseUtils;
+
+/**
+ * Returns an html snippet of the diff in the standard Gitweb style.
+ *
+ * @author James Moger
+ *
+ */
+public class GitWebDiffFormatter extends DiffFormatter {
+
+ private final OutputStream os;
+
+ public GitWebDiffFormatter(OutputStream os) {
+ super(os);
+ this.os = os;
+ }
+
+ /**
+ * Output a hunk header
+ *
+ * @param aStartLine
+ * within first source
+ * @param aEndLine
+ * within first source
+ * @param bStartLine
+ * within second source
+ * @param bEndLine
+ * within second source
+ * @throws IOException
+ */
+ @Override
+ protected void writeHunkHeader(int aStartLine, int aEndLine, int bStartLine, int bEndLine)
+ throws IOException {
+ os.write("<div class=\"diff hunk_header\"><span class=\"diff hunk_info\">".getBytes());
+ os.write('@');
+ os.write('@');
+ writeRange('-', aStartLine + 1, aEndLine - aStartLine);
+ writeRange('+', bStartLine + 1, bEndLine - bStartLine);
+ os.write(' ');
+ os.write('@');
+ os.write('@');
+ os.write("</span></div>".getBytes());
+ }
+
+ protected void writeRange(final char prefix, final int begin, final int cnt) throws IOException {
+ os.write(' ');
+ os.write(prefix);
+ switch (cnt) {
+ case 0:
+ // If the range is empty, its beginning number must
+ // be the
+ // line just before the range, or 0 if the range is
+ // at the
+ // start of the file stream. Here, begin is always 1
+ // based,
+ // so an empty file would produce "0,0".
+ //
+ os.write(encodeASCII(begin - 1));
+ os.write(',');
+ os.write('0');
+ break;
+
+ case 1:
+ // If the range is exactly one line, produce only
+ // the number.
+ //
+ os.write(encodeASCII(begin));
+ break;
+
+ default:
+ os.write(encodeASCII(begin));
+ os.write(',');
+ os.write(encodeASCII(cnt));
+ break;
+ }
+ }
+
+ @Override
+ protected void writeLine(final char prefix, final RawText text, final int cur)
+ throws IOException {
+ switch (prefix) {
+ case '+':
+ os.write("<span style=\"color:#008000;\">".getBytes());
+ break;
+ case '-':
+ os.write("<span style=\"color:#800000;\">".getBytes());
+ break;
+ }
+ os.write(prefix);
+ String line = text.getString(cur);
+ line = StringUtils.escapeForHtml(line, false);
+ os.write(encode(line));
+ switch (prefix) {
+ case '+':
+ case '-':
+ os.write("</span>\n".getBytes());
+ break;
+ default:
+ os.write('\n');
+ }
+ }
+
+ /**
+ * Workaround function for complex private methods in DiffFormatter. This
+ * sets the html for the diff headers.
+ *
+ * @return
+ */
+ public String getHtml() {
+ ByteArrayOutputStream bos = (ByteArrayOutputStream) os;
+ String html = RawParseUtils.decode(bos.toByteArray());
+ String[] lines = html.split("\n");
+ StringBuilder sb = new StringBuilder();
+ sb.append("<div class=\"diff\">");
+ for (String line : lines) {
+ if (line.startsWith("diff")) {
+ sb.append("<div class=\"diff header\">").append(StringUtils.convertOctal(line)).append("</div>");
+ } else if (line.startsWith("---")) {
+ sb.append("<span style=\"color:#800000;\">").append(StringUtils.convertOctal(line)).append("</span><br/>");
+ } else if (line.startsWith("+++")) {
+ sb.append("<span style=\"color:#008000;\">").append(StringUtils.convertOctal(line)).append("</span><br/>");
+ } else {
+ sb.append(line).append('\n');
+ }
+ }
+ sb.append("</div>\n");
+ return sb.toString();
+ }
+}
diff --git a/src/main/java/com/gitblit/utils/HttpUtils.java b/src/main/java/com/gitblit/utils/HttpUtils.java
new file mode 100644
index 00000000..86f53cfe
--- /dev/null
+++ b/src/main/java/com/gitblit/utils/HttpUtils.java
@@ -0,0 +1,204 @@
+/*
+ * 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.security.cert.CertificateExpiredException;
+import java.security.cert.CertificateNotYetValidException;
+import java.security.cert.X509Certificate;
+import java.text.MessageFormat;
+import java.util.Date;
+
+import javax.servlet.http.HttpServletRequest;
+
+import org.slf4j.LoggerFactory;
+
+import com.gitblit.models.UserModel;
+import com.gitblit.utils.X509Utils.X509Metadata;
+
+/**
+ * Collection of utility methods for http requests.
+ *
+ * @author James Moger
+ *
+ */
+public class HttpUtils {
+
+ /**
+ * Returns the Gitblit URL based on the request.
+ *
+ * @param request
+ * @return the host url
+ */
+ public static String getGitblitURL(HttpServletRequest request) {
+ // default to the request scheme and port
+ String scheme = request.getScheme();
+ int port = request.getServerPort();
+
+ // try to use reverse-proxy server's port
+ String forwardedPort = request.getHeader("X-Forwarded-Port");
+ if (StringUtils.isEmpty(forwardedPort)) {
+ forwardedPort = request.getHeader("X_Forwarded_Port");
+ }
+ if (!StringUtils.isEmpty(forwardedPort)) {
+ // reverse-proxy server has supplied the original port
+ try {
+ port = Integer.parseInt(forwardedPort);
+ } catch (Throwable t) {
+ }
+ }
+
+ // try to use reverse-proxy server's scheme
+ String forwardedScheme = request.getHeader("X-Forwarded-Proto");
+ if (StringUtils.isEmpty(forwardedScheme)) {
+ forwardedScheme = request.getHeader("X_Forwarded_Proto");
+ }
+ if (!StringUtils.isEmpty(forwardedScheme)) {
+ // reverse-proxy server has supplied the original scheme
+ scheme = forwardedScheme;
+
+ if ("https".equals(scheme) && port == 80) {
+ // proxy server is https, inside server is 80
+ // this is likely because the proxy server has not supplied
+ // x-forwarded-port. since 80 is almost definitely wrong,
+ // make an educated guess that 443 is correct.
+ port = 443;
+ }
+ }
+
+ String context = request.getContextPath();
+ String forwardedContext = request.getHeader("X-Forwarded-Context");
+ if (forwardedContext != null) {
+ forwardedContext = request.getHeader("X_Forwarded_Context");
+ }
+ if (!StringUtils.isEmpty(forwardedContext)) {
+ context = forwardedContext;
+ }
+
+ // trim any trailing slash
+ if (context.length() > 0 && context.charAt(context.length() - 1) == '/') {
+ context = context.substring(1);
+ }
+
+ StringBuilder sb = new StringBuilder();
+ sb.append(scheme);
+ sb.append("://");
+ sb.append(request.getServerName());
+ if (("http".equals(scheme) && port != 80)
+ || ("https".equals(scheme) && port != 443)) {
+ sb.append(":" + port);
+ }
+ sb.append(context);
+ return sb.toString();
+ }
+
+ /**
+ * Returns a user model object built from attributes in the SSL certificate.
+ * This model is not retrieved from the user service.
+ *
+ * @param httpRequest
+ * @param checkValidity ensure certificate can be used now
+ * @param usernameOIDs if unspecified, CN is used as the username
+ * @return a UserModel, if a valid certificate is in the request, null otherwise
+ */
+ public static UserModel getUserModelFromCertificate(HttpServletRequest httpRequest, boolean checkValidity, String... usernameOIDs) {
+ if (httpRequest.getAttribute("javax.servlet.request.X509Certificate") != null) {
+ X509Certificate[] certChain = (X509Certificate[]) httpRequest
+ .getAttribute("javax.servlet.request.X509Certificate");
+ if (certChain != null) {
+ X509Certificate cert = certChain[0];
+ // ensure certificate is valid
+ if (checkValidity) {
+ try {
+ cert.checkValidity(new Date());
+ } catch (CertificateNotYetValidException e) {
+ LoggerFactory.getLogger(HttpUtils.class).info(MessageFormat.format("X509 certificate {0} is not yet valid", cert.getSubjectDN().getName()));
+ return null;
+ } catch (CertificateExpiredException e) {
+ LoggerFactory.getLogger(HttpUtils.class).info(MessageFormat.format("X509 certificate {0} has expired", cert.getSubjectDN().getName()));
+ return null;
+ }
+ }
+ return getUserModelFromCertificate(cert, usernameOIDs);
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Creates a UserModel from a certificate
+ * @param cert
+ * @param usernameOids if unspecified CN is used as the username
+ * @return
+ */
+ public static UserModel getUserModelFromCertificate(X509Certificate cert, String... usernameOIDs) {
+ X509Metadata metadata = X509Utils.getMetadata(cert);
+
+ UserModel user = new UserModel(metadata.commonName);
+ user.emailAddress = metadata.emailAddress;
+ user.isAuthenticated = false;
+
+ if (usernameOIDs == null || usernameOIDs.length == 0) {
+ // use default usename<->CN mapping
+ usernameOIDs = new String [] { "CN" };
+ }
+
+ // determine username from OID fingerprint
+ StringBuilder an = new StringBuilder();
+ for (String oid : usernameOIDs) {
+ String val = metadata.getOID(oid.toUpperCase(), null);
+ if (val != null) {
+ an.append(val).append(' ');
+ }
+ }
+ user.username = an.toString().trim();
+ return user;
+ }
+
+ public static X509Metadata getCertificateMetadata(HttpServletRequest httpRequest) {
+ if (httpRequest.getAttribute("javax.servlet.request.X509Certificate") != null) {
+ X509Certificate[] certChain = (X509Certificate[]) httpRequest
+ .getAttribute("javax.servlet.request.X509Certificate");
+ if (certChain != null) {
+ X509Certificate cert = certChain[0];
+ return X509Utils.getMetadata(cert);
+ }
+ }
+ return null;
+ }
+
+ public static boolean isIpAddress(String address) {
+ if (StringUtils.isEmpty(address)) {
+ return false;
+ }
+ String [] fields = address.split("\\.");
+ if (fields.length == 4) {
+ // IPV4
+ for (String field : fields) {
+ try {
+ int value = Integer.parseInt(field);
+ if (value < 0 || value > 255) {
+ return false;
+ }
+ } catch (Exception e) {
+ return false;
+ }
+ }
+ return true;
+ }
+ // TODO IPV6?
+ return false;
+ }
+}
diff --git a/src/main/java/com/gitblit/utils/IssueUtils.java b/src/main/java/com/gitblit/utils/IssueUtils.java
new file mode 100644
index 00000000..dd09235b
--- /dev/null
+++ b/src/main/java/com/gitblit/utils/IssueUtils.java
@@ -0,0 +1,829 @@
+/*
+ * Copyright 2012 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.IOException;
+import java.text.MessageFormat;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.TreeSet;
+
+import org.eclipse.jgit.api.errors.ConcurrentRefUpdateException;
+import org.eclipse.jgit.api.errors.JGitInternalException;
+import org.eclipse.jgit.dircache.DirCache;
+import org.eclipse.jgit.dircache.DirCacheBuilder;
+import org.eclipse.jgit.dircache.DirCacheEntry;
+import org.eclipse.jgit.internal.JGitText;
+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.PersonIdent;
+import org.eclipse.jgit.lib.RefUpdate;
+import org.eclipse.jgit.lib.RefUpdate.Result;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.revwalk.RevCommit;
+import org.eclipse.jgit.revwalk.RevTree;
+import org.eclipse.jgit.revwalk.RevWalk;
+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.PathFilterGroup;
+import org.eclipse.jgit.treewalk.filter.TreeFilter;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.gitblit.models.IssueModel;
+import com.gitblit.models.IssueModel.Attachment;
+import com.gitblit.models.IssueModel.Change;
+import com.gitblit.models.IssueModel.Field;
+import com.gitblit.models.IssueModel.Status;
+import com.gitblit.models.RefModel;
+import com.gitblit.utils.JsonUtils.ExcludeField;
+import com.google.gson.Gson;
+import com.google.gson.reflect.TypeToken;
+
+/**
+ * Utility class for reading Gitblit issues.
+ *
+ * @author James Moger
+ *
+ */
+public class IssueUtils {
+
+ public static interface IssueFilter {
+ public abstract boolean accept(IssueModel issue);
+ }
+
+ public static final String GB_ISSUES = "refs/gitblit/issues";
+
+ static final Logger LOGGER = LoggerFactory.getLogger(IssueUtils.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<Object> parameters = new ArrayList<Object>();
+ 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 a RefModel for the gb-issues branch in the repository. If the
+ * branch can not be found, null is returned.
+ *
+ * @param repository
+ * @return a refmodel for the gb-issues branch or null
+ */
+ public static RefModel getIssuesBranch(Repository repository) {
+ List<RefModel> refs = JGitUtils.getRefs(repository, com.gitblit.Constants.R_GITBLIT);
+ for (RefModel ref : refs) {
+ if (ref.reference.getName().equals(GB_ISSUES)) {
+ return ref;
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Returns all the issues in the repository. Querying issues from the
+ * repository requires deserializing all changes for all issues. This is an
+ * expensive process and not recommended. Issues should be indexed by Lucene
+ * and queries should be executed against that index.
+ *
+ * @param repository
+ * @param filter
+ * optional issue filter to only return matching results
+ * @return a list of issues
+ */
+ public static List<IssueModel> getIssues(Repository repository, IssueFilter filter) {
+ List<IssueModel> list = new ArrayList<IssueModel>();
+ RefModel issuesBranch = getIssuesBranch(repository);
+ if (issuesBranch == null) {
+ return list;
+ }
+
+ // Collect the set of all issue paths
+ Set<String> issuePaths = new HashSet<String>();
+ final TreeWalk tw = new TreeWalk(repository);
+ try {
+ RevCommit head = JGitUtils.getCommit(repository, GB_ISSUES);
+ tw.addTree(head.getTree());
+ tw.setRecursive(false);
+ while (tw.next()) {
+ if (tw.getDepth() < 2 && tw.isSubtree()) {
+ tw.enterSubtree();
+ if (tw.getDepth() == 2) {
+ issuePaths.add(tw.getPathString());
+ }
+ }
+ }
+ } catch (IOException e) {
+ error(e, repository, "{0} failed to query issues");
+ } finally {
+ tw.release();
+ }
+
+ // Build each issue and optionally filter out unwanted issues
+
+ for (String issuePath : issuePaths) {
+ RevWalk rw = new RevWalk(repository);
+ try {
+ RevCommit start = rw.parseCommit(repository.resolve(GB_ISSUES));
+ rw.markStart(start);
+ } catch (Exception e) {
+ error(e, repository, "Failed to find {1} in {0}", GB_ISSUES);
+ }
+ TreeFilter treeFilter = AndTreeFilter.create(
+ PathFilterGroup.createFromStrings(issuePath), TreeFilter.ANY_DIFF);
+ rw.setTreeFilter(treeFilter);
+ Iterator<RevCommit> revlog = rw.iterator();
+
+ List<RevCommit> commits = new ArrayList<RevCommit>();
+ while (revlog.hasNext()) {
+ commits.add(revlog.next());
+ }
+
+ // release the revwalk
+ rw.release();
+
+ if (commits.size() == 0) {
+ LOGGER.warn("Failed to find changes for issue " + issuePath);
+ continue;
+ }
+
+ // sort by commit order, first commit first
+ Collections.reverse(commits);
+
+ StringBuilder sb = new StringBuilder("[");
+ boolean first = true;
+ for (RevCommit commit : commits) {
+ if (!first) {
+ sb.append(',');
+ }
+ String message = commit.getFullMessage();
+ // commit message is formatted: C ISSUEID\n\nJSON
+ // C is an single char commit code
+ // ISSUEID is an SHA-1 hash
+ String json = message.substring(43);
+ sb.append(json);
+ first = false;
+ }
+ sb.append(']');
+
+ // Deserialize the JSON array as a Collection<Change>, this seems
+ // slightly faster than deserializing each change by itself.
+ Collection<Change> changes = JsonUtils.fromJsonString(sb.toString(),
+ new TypeToken<Collection<Change>>() {
+ }.getType());
+
+ // create an issue object form the changes
+ IssueModel issue = buildIssue(changes, true);
+
+ // add the issue, conditionally, to the list
+ if (filter == null) {
+ list.add(issue);
+ } else {
+ if (filter.accept(issue)) {
+ list.add(issue);
+ }
+ }
+ }
+
+ // sort the issues by creation
+ Collections.sort(list);
+ return list;
+ }
+
+ /**
+ * Retrieves the specified issue from the repository with all changes
+ * applied to build the effective issue.
+ *
+ * @param repository
+ * @param issueId
+ * @return an issue, if it exists, otherwise null
+ */
+ public static IssueModel getIssue(Repository repository, String issueId) {
+ return getIssue(repository, issueId, true);
+ }
+
+ /**
+ * Retrieves the specified issue from the repository.
+ *
+ * @param repository
+ * @param issueId
+ * @param effective
+ * if true, the effective issue is built by processing comment
+ * changes, deletions, etc. if false, the raw issue is built
+ * without consideration for comment changes, deletions, etc.
+ * @return an issue, if it exists, otherwise null
+ */
+ public static IssueModel getIssue(Repository repository, String issueId, boolean effective) {
+ RefModel issuesBranch = getIssuesBranch(repository);
+ if (issuesBranch == null) {
+ return null;
+ }
+
+ if (StringUtils.isEmpty(issueId)) {
+ return null;
+ }
+
+ String issuePath = getIssuePath(issueId);
+
+ // Collect all changes as JSON array from commit messages
+ List<RevCommit> commits = JGitUtils.getRevLog(repository, GB_ISSUES, issuePath, 0, -1);
+
+ // sort by commit order, first commit first
+ Collections.reverse(commits);
+
+ StringBuilder sb = new StringBuilder("[");
+ boolean first = true;
+ for (RevCommit commit : commits) {
+ if (!first) {
+ sb.append(',');
+ }
+ String message = commit.getFullMessage();
+ // commit message is formatted: C ISSUEID\n\nJSON
+ // C is an single char commit code
+ // ISSUEID is an SHA-1 hash
+ String json = message.substring(43);
+ sb.append(json);
+ first = false;
+ }
+ sb.append(']');
+
+ // Deserialize the JSON array as a Collection<Change>, this seems
+ // slightly faster than deserializing each change by itself.
+ Collection<Change> changes = JsonUtils.fromJsonString(sb.toString(),
+ new TypeToken<Collection<Change>>() {
+ }.getType());
+
+ // create an issue object and apply the changes to it
+ IssueModel issue = buildIssue(changes, effective);
+ return issue;
+ }
+
+ /**
+ * Builds an issue from a set of changes.
+ *
+ * @param changes
+ * @param effective
+ * if true, the effective issue is built which accounts for
+ * comment changes, comment deletions, etc. if false, the raw
+ * issue is built.
+ * @return an issue
+ */
+ private static IssueModel buildIssue(Collection<Change> changes, boolean effective) {
+ IssueModel issue;
+ if (effective) {
+ List<Change> effectiveChanges = new ArrayList<Change>();
+ Map<String, Change> comments = new HashMap<String, Change>();
+ for (Change change : changes) {
+ if (change.comment != null) {
+ if (comments.containsKey(change.comment.id)) {
+ Change original = comments.get(change.comment.id);
+ Change clone = DeepCopier.copy(original);
+ clone.comment.text = change.comment.text;
+ clone.comment.deleted = change.comment.deleted;
+ int idx = effectiveChanges.indexOf(original);
+ effectiveChanges.remove(original);
+ effectiveChanges.add(idx, clone);
+ comments.put(clone.comment.id, clone);
+ } else {
+ effectiveChanges.add(change);
+ comments.put(change.comment.id, change);
+ }
+ } else {
+ effectiveChanges.add(change);
+ }
+ }
+
+ // effective issue
+ issue = new IssueModel();
+ for (Change change : effectiveChanges) {
+ issue.applyChange(change);
+ }
+ } else {
+ // raw issue
+ issue = new IssueModel();
+ for (Change change : changes) {
+ issue.applyChange(change);
+ }
+ }
+ return issue;
+ }
+
+ /**
+ * Retrieves the specified attachment from an issue.
+ *
+ * @param repository
+ * @param issueId
+ * @param filename
+ * @return an attachment, if found, null otherwise
+ */
+ public static Attachment getIssueAttachment(Repository repository, String issueId,
+ String filename) {
+ RefModel issuesBranch = getIssuesBranch(repository);
+ if (issuesBranch == null) {
+ return null;
+ }
+
+ if (StringUtils.isEmpty(issueId)) {
+ return null;
+ }
+
+ // deserialize the issue model so that we have the attachment metadata
+ IssueModel issue = getIssue(repository, issueId, true);
+ Attachment attachment = issue.getAttachment(filename);
+
+ // attachment not found
+ if (attachment == null) {
+ return null;
+ }
+
+ // retrieve the attachment content
+ String issuePath = getIssuePath(issueId);
+ RevTree tree = JGitUtils.getCommit(repository, GB_ISSUES).getTree();
+ byte[] content = JGitUtils
+ .getByteContent(repository, tree, issuePath + "/" + attachment.id, false);
+ attachment.content = content;
+ attachment.size = content.length;
+ return attachment;
+ }
+
+ /**
+ * Creates an issue in the gb-issues branch of the repository. The branch is
+ * automatically created if it does not already exist. Your change must
+ * include an author, summary, and description, at a minimum. If your change
+ * does not have those minimum requirements a RuntimeException will be
+ * thrown.
+ *
+ * @param repository
+ * @param change
+ * @return true if successful
+ */
+ public static IssueModel createIssue(Repository repository, Change change) {
+ RefModel issuesBranch = getIssuesBranch(repository);
+ if (issuesBranch == null) {
+ JGitUtils.createOrphanBranch(repository, GB_ISSUES, null);
+ }
+
+ if (StringUtils.isEmpty(change.author)) {
+ throw new RuntimeException("Must specify a change author!");
+ }
+ if (!change.hasField(Field.Summary)) {
+ throw new RuntimeException("Must specify a summary!");
+ }
+ if (!change.hasField(Field.Description)) {
+ throw new RuntimeException("Must specify a description!");
+ }
+
+ change.setField(Field.Reporter, change.author);
+
+ String issueId = StringUtils.getSHA1(change.created.toString() + change.author
+ + change.getString(Field.Summary) + change.getField(Field.Description));
+ change.setField(Field.Id, issueId);
+ change.code = '+';
+
+ boolean success = commit(repository, issueId, change);
+ if (success) {
+ return getIssue(repository, issueId, false);
+ }
+ return null;
+ }
+
+ /**
+ * Updates an issue in the gb-issues branch of the repository.
+ *
+ * @param repository
+ * @param issueId
+ * @param change
+ * @return true if successful
+ */
+ public static boolean updateIssue(Repository repository, String issueId, Change change) {
+ boolean success = false;
+ RefModel issuesBranch = getIssuesBranch(repository);
+
+ if (issuesBranch == null) {
+ throw new RuntimeException("gb-issues branch does not exist!");
+ }
+
+ if (change == null) {
+ throw new RuntimeException("change can not be null!");
+ }
+
+ if (StringUtils.isEmpty(change.author)) {
+ throw new RuntimeException("must specify a change author!");
+ }
+
+ // determine update code
+ // default update code is '=' for a general change
+ change.code = '=';
+ if (change.hasField(Field.Status)) {
+ Status status = Status.fromObject(change.getField(Field.Status));
+ if (status.isClosed()) {
+ // someone closed the issue
+ change.code = 'x';
+ }
+ }
+ success = commit(repository, issueId, change);
+ return success;
+ }
+
+ /**
+ * Deletes an issue from the repository.
+ *
+ * @param repository
+ * @param issueId
+ * @return true if successful
+ */
+ public static boolean deleteIssue(Repository repository, String issueId, String author) {
+ boolean success = false;
+ RefModel issuesBranch = getIssuesBranch(repository);
+
+ if (issuesBranch == null) {
+ throw new RuntimeException(GB_ISSUES + " does not exist!");
+ }
+
+ if (StringUtils.isEmpty(issueId)) {
+ throw new RuntimeException("must specify an issue id!");
+ }
+
+ String issuePath = getIssuePath(issueId);
+
+ String message = "- " + issueId;
+ try {
+ ObjectId headId = repository.resolve(GB_ISSUES + "^{commit}");
+ ObjectInserter odi = repository.newObjectInserter();
+ try {
+ // Create the in-memory index of the new/updated issue
+ DirCache index = DirCache.newInCore();
+ DirCacheBuilder dcBuilder = index.builder();
+ // Traverse HEAD to add all other paths
+ TreeWalk treeWalk = new TreeWalk(repository);
+ int hIdx = -1;
+ if (headId != null)
+ hIdx = treeWalk.addTree(new RevWalk(repository).parseTree(headId));
+ treeWalk.setRecursive(true);
+ while (treeWalk.next()) {
+ String path = treeWalk.getPathString();
+ CanonicalTreeParser hTree = null;
+ if (hIdx != -1)
+ hTree = treeWalk.getTree(hIdx, CanonicalTreeParser.class);
+ if (!path.startsWith(issuePath)) {
+ // add entries from HEAD for all other paths
+ if (hTree != null) {
+ // create a new DirCacheEntry with data retrieved
+ // from HEAD
+ final DirCacheEntry dcEntry = new DirCacheEntry(path);
+ dcEntry.setObjectId(hTree.getEntryObjectId());
+ dcEntry.setFileMode(hTree.getEntryFileMode());
+
+ // add to temporary in-core index
+ dcBuilder.add(dcEntry);
+ }
+ }
+ }
+
+ // release the treewalk
+ treeWalk.release();
+
+ // finish temporary in-core index used for this commit
+ dcBuilder.finish();
+
+ ObjectId indexTreeId = index.writeTree(odi);
+
+ // Create a commit object
+ PersonIdent ident = new PersonIdent(author, "gitblit@localhost");
+ CommitBuilder commit = new CommitBuilder();
+ commit.setAuthor(ident);
+ commit.setCommitter(ident);
+ commit.setEncoding(Constants.CHARACTER_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(repository);
+ try {
+ RevCommit revCommit = revWalk.parseCommit(commitId);
+ RefUpdate ru = repository.updateRef(GB_ISSUES);
+ ru.setNewObjectId(commitId);
+ ru.setExpectedOldObjectId(headId);
+ ru.setRefLogMessage("commit: " + revCommit.getShortMessage(), false);
+ Result rc = ru.forceUpdate();
+ 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, GB_ISSUES, commitId.toString(),
+ rc));
+ }
+ } finally {
+ revWalk.release();
+ }
+ } finally {
+ odi.release();
+ }
+ } catch (Throwable t) {
+ error(t, repository, "Failed to delete issue {1} to {0}", issueId);
+ }
+ return success;
+ }
+
+ /**
+ * Changes the text of an issue comment.
+ *
+ * @param repository
+ * @param issue
+ * @param change
+ * the change with the comment to change
+ * @param author
+ * the author of the revision
+ * @param comment
+ * the revised comment
+ * @return true, if the change was successful
+ */
+ public static boolean changeComment(Repository repository, IssueModel issue, Change change,
+ String author, String comment) {
+ Change revision = new Change(author);
+ revision.comment(comment);
+ revision.comment.id = change.comment.id;
+ return updateIssue(repository, issue.id, revision);
+ }
+
+ /**
+ * Deletes a comment from an issue.
+ *
+ * @param repository
+ * @param issue
+ * @param change
+ * the change with the comment to delete
+ * @param author
+ * @return true, if the deletion was successful
+ */
+ public static boolean deleteComment(Repository repository, IssueModel issue, Change change,
+ String author) {
+ Change deletion = new Change(author);
+ deletion.comment(change.comment.text);
+ deletion.comment.id = change.comment.id;
+ deletion.comment.deleted = true;
+ return updateIssue(repository, issue.id, deletion);
+ }
+
+ /**
+ * Commit a change to the repository. Each issue is composed on changes.
+ * Issues are built from applying the changes in the order they were
+ * committed to the repository. The changes are actually specified in the
+ * commit messages and not in the RevTrees which allows for clean,
+ * distributed merging.
+ *
+ * @param repository
+ * @param issueId
+ * @param change
+ * @return true, if the change was committed
+ */
+ private static boolean commit(Repository repository, String issueId, Change change) {
+ boolean success = false;
+
+ try {
+ // assign ids to new attachments
+ // attachments are stored by an SHA1 id
+ if (change.hasAttachments()) {
+ for (Attachment attachment : change.attachments) {
+ if (!ArrayUtils.isEmpty(attachment.content)) {
+ byte[] prefix = (change.created.toString() + change.author).getBytes();
+ byte[] bytes = new byte[prefix.length + attachment.content.length];
+ System.arraycopy(prefix, 0, bytes, 0, prefix.length);
+ System.arraycopy(attachment.content, 0, bytes, prefix.length,
+ attachment.content.length);
+ attachment.id = "attachment-" + StringUtils.getSHA1(bytes);
+ }
+ }
+ }
+
+ // serialize the change as json
+ // exclude any attachment from json serialization
+ Gson gson = JsonUtils.gson(new ExcludeField(
+ "com.gitblit.models.IssueModel$Attachment.content"));
+ String json = gson.toJson(change);
+
+ // include the json change in the commit message
+ String issuePath = getIssuePath(issueId);
+ String message = change.code + " " + issueId + "\n\n" + json;
+
+ // Create a commit file. This is required for a proper commit and
+ // ensures we can retrieve the commit log of the issue path.
+ //
+ // This file is NOT serialized as part of the Change object.
+ switch (change.code) {
+ case '+': {
+ // New Issue.
+ Attachment placeholder = new Attachment("issue");
+ placeholder.id = placeholder.name;
+ placeholder.content = "DO NOT REMOVE".getBytes(Constants.CHARACTER_ENCODING);
+ change.addAttachment(placeholder);
+ break;
+ }
+ default: {
+ // Update Issue.
+ String changeId = StringUtils.getSHA1(json);
+ Attachment placeholder = new Attachment("change-" + changeId);
+ placeholder.id = placeholder.name;
+ placeholder.content = "REMOVABLE".getBytes(Constants.CHARACTER_ENCODING);
+ change.addAttachment(placeholder);
+ break;
+ }
+ }
+
+ ObjectId headId = repository.resolve(GB_ISSUES + "^{commit}");
+ ObjectInserter odi = repository.newObjectInserter();
+ try {
+ // Create the in-memory index of the new/updated issue
+ DirCache index = createIndex(repository, headId, issuePath, change);
+ ObjectId indexTreeId = index.writeTree(odi);
+
+ // Create a commit object
+ PersonIdent ident = new PersonIdent(change.author, "gitblit@localhost");
+ CommitBuilder commit = new CommitBuilder();
+ commit.setAuthor(ident);
+ commit.setCommitter(ident);
+ commit.setEncoding(Constants.CHARACTER_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(repository);
+ try {
+ RevCommit revCommit = revWalk.parseCommit(commitId);
+ RefUpdate ru = repository.updateRef(GB_ISSUES);
+ ru.setNewObjectId(commitId);
+ ru.setExpectedOldObjectId(headId);
+ ru.setRefLogMessage("commit: " + revCommit.getShortMessage(), false);
+ Result rc = ru.forceUpdate();
+ 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, GB_ISSUES, commitId.toString(),
+ rc));
+ }
+ } finally {
+ revWalk.release();
+ }
+ } finally {
+ odi.release();
+ }
+ } catch (Throwable t) {
+ error(t, repository, "Failed to commit issue {1} to {0}", issueId);
+ }
+ return success;
+ }
+
+ /**
+ * Returns the issue path. This follows the same scheme as Git's object
+ * store path where the first two characters of the hash id are the root
+ * folder with the remaining characters as a subfolder within that folder.
+ *
+ * @param issueId
+ * @return the root path of the issue content on the gb-issues branch
+ */
+ static String getIssuePath(String issueId) {
+ return issueId.substring(0, 2) + "/" + issueId.substring(2);
+ }
+
+ /**
+ * Creates an in-memory index of the issue change.
+ *
+ * @param repo
+ * @param headId
+ * @param change
+ * @return an in-memory index
+ * @throws IOException
+ */
+ private static DirCache createIndex(Repository repo, ObjectId headId, String issuePath,
+ Change change) throws IOException {
+
+ DirCache inCoreIndex = DirCache.newInCore();
+ DirCacheBuilder dcBuilder = inCoreIndex.builder();
+ ObjectInserter inserter = repo.newObjectInserter();
+
+ Set<String> ignorePaths = new TreeSet<String>();
+ try {
+ // Add any attachments to the temporary index
+ if (change.hasAttachments()) {
+ for (Attachment attachment : change.attachments) {
+ // build a path name for the attachment and mark as ignored
+ String path = issuePath + "/" + attachment.id;
+ ignorePaths.add(path);
+
+ // create an index entry for this attachment
+ final DirCacheEntry dcEntry = new DirCacheEntry(path);
+ dcEntry.setLength(attachment.content.length);
+ dcEntry.setLastModified(change.created.getTime());
+ dcEntry.setFileMode(FileMode.REGULAR_FILE);
+
+ // insert object
+ dcEntry.setObjectId(inserter.insert(Constants.OBJ_BLOB, attachment.content));
+
+ // add to temporary in-core index
+ dcBuilder.add(dcEntry);
+ }
+ }
+
+ // Traverse HEAD to add all other paths
+ TreeWalk treeWalk = new TreeWalk(repo);
+ int hIdx = -1;
+ if (headId != null)
+ hIdx = treeWalk.addTree(new RevWalk(repo).parseTree(headId));
+ treeWalk.setRecursive(true);
+
+ while (treeWalk.next()) {
+ String path = treeWalk.getPathString();
+ CanonicalTreeParser hTree = null;
+ if (hIdx != -1)
+ hTree = treeWalk.getTree(hIdx, CanonicalTreeParser.class);
+ if (!ignorePaths.contains(path)) {
+ // add entries from HEAD for all other paths
+ if (hTree != null) {
+ // create a new DirCacheEntry with data retrieved from
+ // HEAD
+ final DirCacheEntry dcEntry = new DirCacheEntry(path);
+ dcEntry.setObjectId(hTree.getEntryObjectId());
+ dcEntry.setFileMode(hTree.getEntryFileMode());
+
+ // add to temporary in-core index
+ dcBuilder.add(dcEntry);
+ }
+ }
+ }
+
+ // release the treewalk
+ treeWalk.release();
+
+ // finish temporary in-core index used for this commit
+ dcBuilder.finish();
+ } finally {
+ inserter.release();
+ }
+ return inCoreIndex;
+ }
+} \ No newline at end of file
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 00000000..1f2ae943
--- /dev/null
+++ b/src/main/java/com/gitblit/utils/JGitUtils.java
@@ -0,0 +1,1775 @@
+/*
+ * 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.ByteArrayOutputStream;
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.text.MessageFormat;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.regex.Pattern;
+
+import org.eclipse.jgit.api.CloneCommand;
+import org.eclipse.jgit.api.FetchCommand;
+import org.eclipse.jgit.api.Git;
+import org.eclipse.jgit.api.errors.GitAPIException;
+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.errors.ConfigInvalidException;
+import org.eclipse.jgit.errors.IncorrectObjectTypeException;
+import org.eclipse.jgit.errors.MissingObjectException;
+import org.eclipse.jgit.errors.StopWalkException;
+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.TreeFormatter;
+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.FileRepository;
+import org.eclipse.jgit.transport.CredentialsProvider;
+import org.eclipse.jgit.transport.FetchResult;
+import org.eclipse.jgit.transport.RefSpec;
+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.eclipse.jgit.util.io.DisabledOutputStream;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.gitblit.models.GitNote;
+import com.gitblit.models.PathModel;
+import com.gitblit.models.PathModel.PathChangeModel;
+import com.gitblit.models.RefModel;
+import com.gitblit.models.SubmoduleModel;
+
+/**
+ * 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<Object> parameters = new ArrayList<Object>();
+ 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 <email
+ * address>". If the email address is empty, just "Real Name" is returned.
+ *
+ * @param person
+ * @return "Real Name <email address>" 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);
+ FileRepository repository = new FileRepository(gitDir);
+ 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<RefSpec> specs = new ArrayList<RefSpec>();
+ 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) {
+ try {
+ Git git = Git.init().setDirectory(new File(repositoriesFolder, name)).setBare(true).call();
+ return git.getRepository();
+ } catch (GitAPIException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ /**
+ * 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<String> getRepositoryList(File repositoriesFolder, boolean onlyBare,
+ boolean searchSubfolders, int depth, List<String> exclusions) {
+ List<String> list = new ArrayList<String>();
+ if (repositoriesFolder == null || !repositoriesFolder.exists()) {
+ return list;
+ }
+ List<Pattern> patterns = new ArrayList<Pattern>();
+ 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);
+ 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<String> getRepositoryList(String basePath, File searchFolder,
+ boolean onlyBare, boolean searchSubfolders, int depth, List<Pattern> patterns) {
+ File baseFile = new File(basePath);
+ List<String> list = new ArrayList<String>();
+ 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;
+ }
+
+ /**
+ * Returns the date 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
+ */
+ public static Date getLastChange(Repository repository) {
+ if (!hasCommits(repository)) {
+ // null repository
+ if (repository == null) {
+ return new Date(0);
+ }
+ // fresh repository
+ return new Date(repository.getDirectory().lastModified());
+ }
+
+ List<RefModel> branchModels = getLocalBranches(repository, true, -1);
+ if (branchModels.size() > 0) {
+ // find most recent branch update
+ Date lastChange = new Date(0);
+ for (RefModel branchModel : branchModels) {
+ if (branchModel.getDate().after(lastChange)) {
+ lastChange = branchModel.getDate();
+ }
+ }
+ return lastChange;
+ }
+
+ // default to the repository folder modification date
+ return new Date(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);
+ }
+ return commit.getAuthorIdent().getWhen();
+ }
+
+ /**
+ * 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;
+ try {
+ // resolve object id
+ ObjectId branchObject;
+ if (StringUtils.isEmpty(objectId)) {
+ branchObject = getDefaultBranch(repository);
+ } else {
+ branchObject = repository.resolve(objectId);
+ }
+ RevWalk walk = new RevWalk(repository);
+ RevCommit rev = walk.parseCommit(branchObject);
+ commit = rev;
+ walk.dispose();
+ } catch (Throwable t) {
+ error(t, repository, "{0} failed to get commit {1}", objectId);
+ }
+ 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);
+ 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) {
+ RevObject ro = rw.lookupAny(entid, entmode.getObjectType());
+ rw.parseBody(ro);
+ ByteArrayOutputStream os = new ByteArrayOutputStream();
+ ObjectLoader ldr = repository.open(ro.getId(), Constants.OBJ_BLOB);
+ byte[] tmp = new byte[4096];
+ InputStream in = ldr.openStream();
+ int n;
+ while ((n = in.read(tmp)) > 0) {
+ os.write(tmp, 0, n);
+ }
+ in.close();
+ content = os.toByteArray();
+ }
+ }
+ } catch (Throwable t) {
+ if (throwError) {
+ error(t, repository, "{0} can't find {1} in tree {2}", path, tree.name());
+ }
+ } finally {
+ rw.dispose();
+ tw.release();
+ }
+ 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));
+ rw.parseBody(blob);
+ ByteArrayOutputStream os = new ByteArrayOutputStream();
+ ObjectLoader ldr = repository.open(blob.getId(), Constants.OBJ_BLOB);
+ byte[] tmp = new byte[4096];
+ InputStream in = ldr.openStream();
+ int n;
+ while ((n = in.read(tmp)) > 0) {
+ os.write(tmp, 0, n);
+ }
+ in.close();
+ content = os.toByteArray();
+ } 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<PathModel> getFilesInPath(Repository repository, String path,
+ RevCommit commit) {
+ List<PathModel> list = new ArrayList<PathModel>();
+ 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.release();
+ }
+ 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<PathChangeModel> getFilesInCommit(Repository repository, RevCommit commit) {
+ List<PathChangeModel> list = new ArrayList<PathChangeModel>();
+ 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()) {
+ list.add(new PathChangeModel(tw.getPathString(), tw.getPathString(), 0, tw
+ .getRawMode(0), tw.getObjectId(0).getName(), commit.getId().getName(),
+ ChangeType.ADD));
+ }
+ tw.release();
+ } else {
+ RevCommit parent = rw.parseCommit(commit.getParent(0).getId());
+ DiffFormatter df = new DiffFormatter(DisabledOutputStream.INSTANCE);
+ df.setRepository(repository);
+ df.setDiffComparator(RawTextComparator.DEFAULT);
+ df.setDetectRenames(true);
+ List<DiffEntry> diffs = df.scan(parent.getTree(), commit.getTree());
+ for (DiffEntry diff : diffs) {
+ String objectId = diff.getNewId().name();
+ if (diff.getChangeType().equals(ChangeType.DELETE)) {
+ list.add(new PathChangeModel(diff.getOldPath(), diff.getOldPath(), 0, diff
+ .getNewMode().getBits(), objectId, commit.getId().getName(), diff
+ .getChangeType()));
+ } else if (diff.getChangeType().equals(ChangeType.RENAME)) {
+ list.add(new PathChangeModel(diff.getOldPath(), diff.getNewPath(), 0, diff
+ .getNewMode().getBits(), objectId, commit.getId().getName(), diff
+ .getChangeType()));
+ } else {
+ list.add(new PathChangeModel(diff.getNewPath(), diff.getNewPath(), 0, diff
+ .getNewMode().getBits(), objectId, commit.getId().getName(), diff
+ .getChangeType()));
+ }
+ }
+ }
+ } catch (Throwable t) {
+ error(t, repository, "{0} failed to determine files in commit!");
+ } finally {
+ rw.dispose();
+ }
+ 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<PathModel> getDocuments(Repository repository, List<String> 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<PathModel> getDocuments(Repository repository, List<String> extensions,
+ String objectId) {
+ List<PathModel> list = new ArrayList<PathModel>();
+ 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<TreeFilter> suffixFilters = new ArrayList<TreeFilter>();
+ 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.release();
+ }
+ 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);
+ try {
+ if (!tw.isSubtree() && (tw.getFileMode(0) != FileMode.GITLINK)) {
+ size = tw.getObjectReader().getObjectSize(objectId, Constants.OBJ_BLOB);
+ }
+ } catch (Throwable t) {
+ error(t, null, "failed to retrieve blob size for " + tw.getPathString());
+ }
+ return new PathModel(name, tw.getPathString(), size, tw.getFileMode(0).getBits(),
+ objectId.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<RevCommit> getRevLog(Repository repository, String objectId, Date minimumDate) {
+ List<RevCommit> list = new ArrayList<RevCommit>();
+ 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<RevCommit> 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<RevCommit> 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<RevCommit> 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<RevCommit> getRevLog(Repository repository, String objectId, String path,
+ int offset, int maxCount) {
+ List<RevCommit> list = new ArrayList<RevCommit>();
+ if (maxCount == 0) {
+ return list;
+ }
+ if (!hasCommits(repository)) {
+ return list;
+ }
+ try {
+ // resolve branch
+ ObjectId branchObject;
+ if (StringUtils.isEmpty(objectId)) {
+ branchObject = getDefaultBranch(repository);
+ } else {
+ branchObject = repository.resolve(objectId);
+ }
+ if (branchObject == null) {
+ return list;
+ }
+
+ RevWalk rw = new RevWalk(repository);
+ rw.markStart(rw.parseCommit(branchObject));
+ if (!StringUtils.isEmpty(path)) {
+ TreeFilter filter = AndTreeFilter.create(
+ PathFilterGroup.createFromStrings(Collections.singleton(path)),
+ TreeFilter.ANY_DIFF);
+ rw.setTreeFilter(filter);
+ }
+ Iterable<RevCommit> 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<RevCommit> getRevLog(Repository repository, String startRangeId,
+ String endRangeId) {
+ List<RevCommit> list = new ArrayList<RevCommit>();
+ 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<RevCommit> 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<RevCommit> searchRevlogs(Repository repository, String objectId,
+ String value, final com.gitblit.Constants.SearchType type, int offset, int maxCount) {
+ final String lcValue = value.toLowerCase();
+ List<RevCommit> list = new ArrayList<RevCommit>();
+ if (maxCount == 0) {
+ return list;
+ }
+ 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.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<RevCommit> 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<RefModel> 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();
+ if (!target.startsWith(Constants.R_HEADS)) {
+ // refers to an actual commit, probably a tag
+ // find latest tag that matches the commit, if any
+ List<RefModel> tagModels = getTags(repository, true, -1);
+ if (tagModels.size() > 0) {
+ RefModel tag = null;
+ Date lastDate = new Date(0);
+ for (RefModel tagModel : tagModels) {
+ if (tagModel.getReferencedObjectId().getName().equals(target) &&
+ tagModel.getDate().after(lastDate)) {
+ tag = tagModel;
+ lastDate = tag.getDate();
+ }
+ }
+ target = tag.getName();
+ }
+ }
+ } 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_HEADS)) {
+ 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) {
+ String branchName = branch;
+ if (!branchName.startsWith(Constants.R_HEADS)) {
+ branchName = Constants.R_HEADS + branch;
+ }
+
+ try {
+ RefUpdate refUpdate = repository.updateRef(branchName, 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(), branchName, result));
+ }
+ } catch (Throwable t) {
+ error(t, repository, "{0} failed to delete {1}", branchName);
+ }
+ 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<String> getAvailableHeadTargets(Repository repository) {
+ List<String> targets = new ArrayList<String>();
+ 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<ObjectId, List<RefModel>> 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<ObjectId, List<RefModel>> getAllRefs(Repository repository, boolean includeRemoteRefs) {
+ List<RefModel> list = getRefs(repository, org.eclipse.jgit.lib.RefDatabase.ALL, true, -1);
+ Map<ObjectId, List<RefModel>> refs = new HashMap<ObjectId, List<RefModel>>();
+ 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<RefModel>());
+ }
+ 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<RefModel> getTags(Repository repository, boolean fullName, int maxCount) {
+ return getRefs(repository, Constants.R_TAGS, fullName, maxCount);
+ }
+
+ /**
+ * 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<RefModel> 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<RefModel> 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<RefModel> 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<RefModel> 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<RefModel> getRefs(Repository repository, String refs, boolean fullName,
+ int maxCount) {
+ List<RefModel> list = new ArrayList<RefModel>();
+ if (maxCount == 0) {
+ return list;
+ }
+ if (!hasCommits(repository)) {
+ return list;
+ }
+ try {
+ Map<String, Ref> map = repository.getRefDatabase().getRefs(refs);
+ RevWalk rw = new RevWalk(repository);
+ for (Entry<String, Ref> 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) {
+ list = new ArrayList<RefModel>(list.subList(0, maxCount));
+ }
+ } 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<SubmoduleModel> 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<SubmoduleModel> getSubmodules(Repository repository, RevTree tree) {
+ List<SubmoduleModel> list = new ArrayList<SubmoduleModel>();
+ 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.release();
+ }
+ 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<GitNote> getNotesOnCommit(Repository repository, RevCommit commit) {
+ List<GitNote> list = new ArrayList<GitNote>();
+ if (!hasCommits(repository)) {
+ return list;
+ }
+ List<RefModel> 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<RevCommit> 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<RevCommit> 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;
+ }
+
+ /**
+ * 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.release();
+ }
+ } finally {
+ odi.release();
+ }
+ } 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);
+ }
+}
diff --git a/src/main/java/com/gitblit/utils/JsonUtils.java b/src/main/java/com/gitblit/utils/JsonUtils.java
new file mode 100644
index 00000000..24f4ecb8
--- /dev/null
+++ b/src/main/java/com/gitblit/utils/JsonUtils.java
@@ -0,0 +1,346 @@
+/*
+ * 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.BufferedReader;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.OutputStream;
+import java.lang.reflect.Type;
+import java.net.HttpURLConnection;
+import java.net.URLConnection;
+import java.text.DateFormat;
+import java.text.ParseException;
+import java.text.SimpleDateFormat;
+import java.util.Collection;
+import java.util.Date;
+import java.util.Locale;
+import java.util.Map;
+import java.util.TimeZone;
+
+import com.gitblit.Constants.AccessPermission;
+import com.gitblit.GitBlitException.ForbiddenException;
+import com.gitblit.GitBlitException.NotAllowedException;
+import com.gitblit.GitBlitException.UnauthorizedException;
+import com.gitblit.GitBlitException.UnknownRequestException;
+import com.gitblit.models.RepositoryModel;
+import com.gitblit.models.UserModel;
+import com.google.gson.ExclusionStrategy;
+import com.google.gson.FieldAttributes;
+import com.google.gson.Gson;
+import com.google.gson.GsonBuilder;
+import com.google.gson.JsonDeserializationContext;
+import com.google.gson.JsonDeserializer;
+import com.google.gson.JsonElement;
+import com.google.gson.JsonPrimitive;
+import com.google.gson.JsonSerializationContext;
+import com.google.gson.JsonSerializer;
+import com.google.gson.JsonSyntaxException;
+import com.google.gson.reflect.TypeToken;
+
+/**
+ * Utility methods for json calls to a Gitblit server.
+ *
+ * @author James Moger
+ *
+ */
+public class JsonUtils {
+
+ public static final Type REPOSITORIES_TYPE = new TypeToken<Map<String, RepositoryModel>>() {
+ }.getType();
+
+ public static final Type USERS_TYPE = new TypeToken<Collection<UserModel>>() {
+ }.getType();
+
+ /**
+ * Creates JSON from the specified object.
+ *
+ * @param o
+ * @return json
+ */
+ public static String toJsonString(Object o) {
+ String json = gson().toJson(o);
+ return json;
+ }
+
+ /**
+ * Convert a json string to an object of the specified type.
+ *
+ * @param json
+ * @param clazz
+ * @return an object
+ */
+ public static <X> X fromJsonString(String json, Class<X> clazz) {
+ return gson().fromJson(json, clazz);
+ }
+
+ /**
+ * Convert a json string to an object of the specified type.
+ *
+ * @param json
+ * @param clazz
+ * @return an object
+ */
+ public static <X> X fromJsonString(String json, Type type) {
+ return gson().fromJson(json, type);
+ }
+
+ /**
+ * Reads a gson object from the specified url.
+ *
+ * @param url
+ * @param type
+ * @return the deserialized object
+ * @throws {@link IOException}
+ */
+ public static <X> X retrieveJson(String url, Type type) throws IOException,
+ UnauthorizedException {
+ return retrieveJson(url, type, null, null);
+ }
+
+ /**
+ * Reads a gson object from the specified url.
+ *
+ * @param url
+ * @param type
+ * @return the deserialized object
+ * @throws {@link IOException}
+ */
+ public static <X> X retrieveJson(String url, Class<? extends X> clazz) throws IOException,
+ UnauthorizedException {
+ return retrieveJson(url, clazz, null, null);
+ }
+
+ /**
+ * Reads a gson object from the specified url.
+ *
+ * @param url
+ * @param type
+ * @param username
+ * @param password
+ * @return the deserialized object
+ * @throws {@link IOException}
+ */
+ public static <X> X retrieveJson(String url, Type type, String username, char[] password)
+ throws IOException {
+ String json = retrieveJsonString(url, username, password);
+ if (StringUtils.isEmpty(json)) {
+ return null;
+ }
+ return gson().fromJson(json, type);
+ }
+
+ /**
+ * Reads a gson object from the specified url.
+ *
+ * @param url
+ * @param clazz
+ * @param username
+ * @param password
+ * @return the deserialized object
+ * @throws {@link IOException}
+ */
+ public static <X> X retrieveJson(String url, Class<X> clazz, String username, char[] password)
+ throws IOException {
+ String json = retrieveJsonString(url, username, password);
+ if (StringUtils.isEmpty(json)) {
+ return null;
+ }
+ return gson().fromJson(json, clazz);
+ }
+
+ /**
+ * Retrieves a JSON message.
+ *
+ * @param url
+ * @return the JSON message as a string
+ * @throws {@link IOException}
+ */
+ public static String retrieveJsonString(String url, String username, char[] password)
+ throws IOException {
+ try {
+ URLConnection conn = ConnectionUtils.openReadConnection(url, username, password);
+ InputStream is = conn.getInputStream();
+ BufferedReader reader = new BufferedReader(new InputStreamReader(is,
+ ConnectionUtils.CHARSET));
+ StringBuilder json = new StringBuilder();
+ char[] buffer = new char[4096];
+ int len = 0;
+ while ((len = reader.read(buffer)) > -1) {
+ json.append(buffer, 0, len);
+ }
+ is.close();
+ return json.toString();
+ } catch (IOException e) {
+ if (e.getMessage().indexOf("401") > -1) {
+ // unauthorized
+ throw new UnauthorizedException(url);
+ } else if (e.getMessage().indexOf("403") > -1) {
+ // requested url is forbidden by the requesting user
+ throw new ForbiddenException(url);
+ } else if (e.getMessage().indexOf("405") > -1) {
+ // requested url is not allowed by the server
+ throw new NotAllowedException(url);
+ } else if (e.getMessage().indexOf("501") > -1) {
+ // requested url is not recognized by the server
+ throw new UnknownRequestException(url);
+ }
+ throw e;
+ }
+ }
+
+ /**
+ * Sends a JSON message.
+ *
+ * @param url
+ * the url to write to
+ * @param json
+ * the json message to send
+ * @return the http request result code
+ * @throws {@link IOException}
+ */
+ public static int sendJsonString(String url, String json) throws IOException {
+ return sendJsonString(url, json, null, null);
+ }
+
+ /**
+ * Sends a JSON message.
+ *
+ * @param url
+ * the url to write to
+ * @param json
+ * the json message to send
+ * @param username
+ * @param password
+ * @return the http request result code
+ * @throws {@link IOException}
+ */
+ public static int sendJsonString(String url, String json, String username, char[] password)
+ throws IOException {
+ try {
+ byte[] jsonBytes = json.getBytes(ConnectionUtils.CHARSET);
+ URLConnection conn = ConnectionUtils.openConnection(url, username, password);
+ conn.setRequestProperty("Content-Type", "text/plain;charset=" + ConnectionUtils.CHARSET);
+ conn.setRequestProperty("Content-Length", "" + jsonBytes.length);
+
+ // write json body
+ OutputStream os = conn.getOutputStream();
+ os.write(jsonBytes);
+ os.close();
+
+ int status = ((HttpURLConnection) conn).getResponseCode();
+ return status;
+ } catch (IOException e) {
+ if (e.getMessage().indexOf("401") > -1) {
+ // unauthorized
+ throw new UnauthorizedException(url);
+ } else if (e.getMessage().indexOf("403") > -1) {
+ // requested url is forbidden by the requesting user
+ throw new ForbiddenException(url);
+ } else if (e.getMessage().indexOf("405") > -1) {
+ // requested url is not allowed by the server
+ throw new NotAllowedException(url);
+ } else if (e.getMessage().indexOf("501") > -1) {
+ // requested url is not recognized by the server
+ throw new UnknownRequestException(url);
+ }
+ throw e;
+ }
+ }
+
+ // build custom gson instance with GMT date serializer/deserializer
+ // http://code.google.com/p/google-gson/issues/detail?id=281
+ public static Gson gson(ExclusionStrategy... strategies) {
+ GsonBuilder builder = new GsonBuilder();
+ builder.registerTypeAdapter(Date.class, new GmtDateTypeAdapter());
+ builder.registerTypeAdapter(AccessPermission.class, new AccessPermissionTypeAdapter());
+ builder.setPrettyPrinting();
+ if (!ArrayUtils.isEmpty(strategies)) {
+ builder.setExclusionStrategies(strategies);
+ }
+ return builder.create();
+ }
+
+ private static class GmtDateTypeAdapter implements JsonSerializer<Date>, JsonDeserializer<Date> {
+ private final DateFormat dateFormat;
+
+ private GmtDateTypeAdapter() {
+ dateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'", Locale.US);
+ dateFormat.setTimeZone(TimeZone.getTimeZone("UTC"));
+ }
+
+ @Override
+ public synchronized JsonElement serialize(Date date, Type type,
+ JsonSerializationContext jsonSerializationContext) {
+ synchronized (dateFormat) {
+ String dateFormatAsString = dateFormat.format(date);
+ return new JsonPrimitive(dateFormatAsString);
+ }
+ }
+
+ @Override
+ public synchronized Date deserialize(JsonElement jsonElement, Type type,
+ JsonDeserializationContext jsonDeserializationContext) {
+ try {
+ synchronized (dateFormat) {
+ Date date = dateFormat.parse(jsonElement.getAsString());
+ return new Date((date.getTime() / 1000) * 1000);
+ }
+ } catch (ParseException e) {
+ throw new JsonSyntaxException(jsonElement.getAsString(), e);
+ }
+ }
+ }
+
+ private static class AccessPermissionTypeAdapter implements JsonSerializer<AccessPermission>, JsonDeserializer<AccessPermission> {
+
+ private AccessPermissionTypeAdapter() {
+ }
+
+ @Override
+ public synchronized JsonElement serialize(AccessPermission permission, Type type,
+ JsonSerializationContext jsonSerializationContext) {
+ return new JsonPrimitive(permission.code);
+ }
+
+ @Override
+ public synchronized AccessPermission deserialize(JsonElement jsonElement, Type type,
+ JsonDeserializationContext jsonDeserializationContext) {
+ return AccessPermission.fromCode(jsonElement.getAsString());
+ }
+ }
+
+ public static class ExcludeField implements ExclusionStrategy {
+
+ private Class<?> c;
+ private String fieldName;
+
+ public ExcludeField(String fqfn) throws SecurityException, NoSuchFieldException,
+ ClassNotFoundException {
+ this.c = Class.forName(fqfn.substring(0, fqfn.lastIndexOf(".")));
+ this.fieldName = fqfn.substring(fqfn.lastIndexOf(".") + 1);
+ }
+
+ public boolean shouldSkipClass(Class<?> arg0) {
+ return false;
+ }
+
+ public boolean shouldSkipField(FieldAttributes f) {
+ return (f.getDeclaringClass() == c && f.getName().equals(fieldName));
+ }
+ }
+}
diff --git a/src/main/java/com/gitblit/utils/MarkdownUtils.java b/src/main/java/com/gitblit/utils/MarkdownUtils.java
new file mode 100644
index 00000000..0b8c9c57
--- /dev/null
+++ b/src/main/java/com/gitblit/utils/MarkdownUtils.java
@@ -0,0 +1,87 @@
+/*
+ * 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.IOException;
+import java.io.Reader;
+import java.io.StringReader;
+import java.io.StringWriter;
+
+import org.slf4j.LoggerFactory;
+import org.tautua.markdownpapers.Markdown;
+import org.tautua.markdownpapers.parser.ParseException;
+
+/**
+ * Utility methods for transforming raw markdown text to html.
+ *
+ * @author James Moger
+ *
+ */
+public class MarkdownUtils {
+
+ /**
+ * Returns the html version of the markdown source text.
+ *
+ * @param markdown
+ * @return html version of markdown text
+ * @throws java.text.ParseException
+ */
+ public static String transformMarkdown(String markdown) throws java.text.ParseException {
+ try {
+ StringReader reader = new StringReader(markdown);
+ String html = transformMarkdown(reader);
+ reader.close();
+ return html;
+ } catch (IllegalArgumentException e) {
+ throw new java.text.ParseException(e.getMessage(), 0);
+ } catch (NullPointerException p) {
+ throw new java.text.ParseException("Markdown string is null!", 0);
+ }
+ }
+
+ /**
+ * Returns the html version of the markdown source reader. The reader is
+ * closed regardless of success or failure.
+ *
+ * @param markdownReader
+ * @return html version of the markdown text
+ * @throws java.text.ParseException
+ */
+ public static String transformMarkdown(Reader markdownReader) throws java.text.ParseException {
+ // Read raw markdown content and transform it to html
+ StringWriter writer = new StringWriter();
+ try {
+ Markdown md = new Markdown();
+ md.transform(markdownReader, writer);
+ return writer.toString().trim();
+ } catch (StringIndexOutOfBoundsException e) {
+ LoggerFactory.getLogger(MarkdownUtils.class).error("MarkdownPapers failed to parse Markdown!", e);
+ throw new java.text.ParseException(e.getMessage(), 0);
+ } catch (ParseException p) {
+ LoggerFactory.getLogger(MarkdownUtils.class).error("MarkdownPapers failed to parse Markdown!", p);
+ throw new java.text.ParseException(p.getMessage(), 0);
+ } catch (Exception e) {
+ LoggerFactory.getLogger(MarkdownUtils.class).error("MarkdownPapers failed to parse Markdown!", e);
+ throw new java.text.ParseException(e.getMessage(), 0);
+ } finally {
+ try {
+ writer.close();
+ } catch (IOException e) {
+ // IGNORE
+ }
+ }
+ }
+}
diff --git a/src/main/java/com/gitblit/utils/MetricUtils.java b/src/main/java/com/gitblit/utils/MetricUtils.java
new file mode 100644
index 00000000..26e4581c
--- /dev/null
+++ b/src/main/java/com/gitblit/utils/MetricUtils.java
@@ -0,0 +1,233 @@
+/*
+ * 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.text.DateFormat;
+import java.text.MessageFormat;
+import java.text.SimpleDateFormat;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.TimeZone;
+
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.revwalk.RevCommit;
+import org.eclipse.jgit.revwalk.RevWalk;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.gitblit.models.Metric;
+import com.gitblit.models.RefModel;
+
+/**
+ * Utility class for collecting metrics on a branch, tag, or other ref within
+ * the repository.
+ *
+ * @author James Moger
+ *
+ */
+public class MetricUtils {
+
+ private static final Logger LOGGER = LoggerFactory.getLogger(MetricUtils.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<Object> parameters = new ArrayList<Object>();
+ 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 list of metrics for the specified commit reference, branch,
+ * or tag within the repository. If includeTotal is true, the total of all
+ * the metrics will be included as the first element in the returned list.
+ *
+ * If the dateformat is unspecified an attempt is made to determine an
+ * appropriate date format by determining the time difference between the
+ * first commit on the branch and the most recent commit. This assumes that
+ * the commits are linear.
+ *
+ * @param repository
+ * @param objectId
+ * if null or empty, HEAD is assumed.
+ * @param includeTotal
+ * @param dateFormat
+ * @param timezone
+ * @return list of metrics
+ */
+ public static List<Metric> getDateMetrics(Repository repository, String objectId,
+ boolean includeTotal, String dateFormat, TimeZone timezone) {
+ Metric total = new Metric("TOTAL");
+ final Map<String, Metric> metricMap = new HashMap<String, Metric>();
+
+ if (JGitUtils.hasCommits(repository)) {
+ final List<RefModel> tags = JGitUtils.getTags(repository, true, -1);
+ final Map<ObjectId, RefModel> tagMap = new HashMap<ObjectId, RefModel>();
+ for (RefModel tag : tags) {
+ tagMap.put(tag.getReferencedObjectId(), tag);
+ }
+ RevWalk revWalk = null;
+ try {
+ // resolve branch
+ ObjectId branchObject;
+ if (StringUtils.isEmpty(objectId)) {
+ branchObject = JGitUtils.getDefaultBranch(repository);
+ } else {
+ branchObject = repository.resolve(objectId);
+ }
+
+ revWalk = new RevWalk(repository);
+ RevCommit lastCommit = revWalk.parseCommit(branchObject);
+ revWalk.markStart(lastCommit);
+
+ DateFormat df;
+ if (StringUtils.isEmpty(dateFormat)) {
+ // dynamically determine date format
+ RevCommit firstCommit = JGitUtils.getFirstCommit(repository,
+ branchObject.getName());
+ int diffDays = (lastCommit.getCommitTime() - firstCommit.getCommitTime())
+ / (60 * 60 * 24);
+ total.duration = diffDays;
+ if (diffDays <= 365) {
+ // Days
+ df = new SimpleDateFormat("yyyy-MM-dd");
+ } else {
+ // Months
+ df = new SimpleDateFormat("yyyy-MM");
+ }
+ } else {
+ // use specified date format
+ df = new SimpleDateFormat(dateFormat);
+ }
+ df.setTimeZone(timezone);
+
+ Iterable<RevCommit> revlog = revWalk;
+ for (RevCommit rev : revlog) {
+ Date d = JGitUtils.getCommitDate(rev);
+ String p = df.format(d);
+ if (!metricMap.containsKey(p)) {
+ metricMap.put(p, new Metric(p));
+ }
+ Metric m = metricMap.get(p);
+ m.count++;
+ total.count++;
+ if (tagMap.containsKey(rev.getId())) {
+ m.tag++;
+ total.tag++;
+ }
+ }
+ } catch (Throwable t) {
+ error(t, repository, "{0} failed to mine log history for date metrics of {1}",
+ objectId);
+ } finally {
+ if (revWalk != null) {
+ revWalk.dispose();
+ }
+ }
+ }
+ List<String> keys = new ArrayList<String>(metricMap.keySet());
+ Collections.sort(keys);
+ List<Metric> metrics = new ArrayList<Metric>();
+ for (String key : keys) {
+ metrics.add(metricMap.get(key));
+ }
+ if (includeTotal) {
+ metrics.add(0, total);
+ }
+ return metrics;
+ }
+
+ /**
+ * Returns a list of author metrics for the specified repository.
+ *
+ * @param repository
+ * @param objectId
+ * if null or empty, HEAD is assumed.
+ * @param byEmailAddress
+ * group metrics by author email address otherwise by author name
+ * @return list of metrics
+ */
+ public static List<Metric> getAuthorMetrics(Repository repository, String objectId,
+ boolean byEmailAddress) {
+ final Map<String, Metric> metricMap = new HashMap<String, Metric>();
+ if (JGitUtils.hasCommits(repository)) {
+ try {
+ RevWalk walk = new RevWalk(repository);
+ // resolve branch
+ ObjectId branchObject;
+ if (StringUtils.isEmpty(objectId)) {
+ branchObject = JGitUtils.getDefaultBranch(repository);
+ } else {
+ branchObject = repository.resolve(objectId);
+ }
+ RevCommit lastCommit = walk.parseCommit(branchObject);
+ walk.markStart(lastCommit);
+
+ Iterable<RevCommit> revlog = walk;
+ for (RevCommit rev : revlog) {
+ String p;
+ if (byEmailAddress) {
+ p = rev.getAuthorIdent().getEmailAddress().toLowerCase();
+ if (StringUtils.isEmpty(p)) {
+ p = rev.getAuthorIdent().getName().toLowerCase();
+ }
+ } else {
+ p = rev.getAuthorIdent().getName().toLowerCase();
+ if (StringUtils.isEmpty(p)) {
+ p = rev.getAuthorIdent().getEmailAddress().toLowerCase();
+ }
+ }
+ p = p.replace('\n',' ').replace('\r', ' ').trim();
+ if (!metricMap.containsKey(p)) {
+ metricMap.put(p, new Metric(p));
+ }
+ Metric m = metricMap.get(p);
+ m.count++;
+ }
+ } catch (Throwable t) {
+ error(t, repository, "{0} failed to mine log history for author metrics of {1}",
+ objectId);
+ }
+ }
+ List<String> keys = new ArrayList<String>(metricMap.keySet());
+ Collections.sort(keys);
+ List<Metric> metrics = new ArrayList<Metric>();
+ for (String key : keys) {
+ metrics.add(metricMap.get(key));
+ }
+ return metrics;
+ }
+}
diff --git a/src/main/java/com/gitblit/utils/ObjectCache.java b/src/main/java/com/gitblit/utils/ObjectCache.java
new file mode 100644
index 00000000..38f2e59a
--- /dev/null
+++ b/src/main/java/com/gitblit/utils/ObjectCache.java
@@ -0,0 +1,98 @@
+/*
+ * 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.Serializable;
+import java.util.Date;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+
+/**
+ * Reusable coarse date-based object cache. The date precision is in
+ * milliseconds and in fast, concurrent systems this cache is too simplistic.
+ * However, for the cases where its being used in Gitblit this cache technique
+ * is just fine.
+ *
+ * @author James Moger
+ *
+ */
+public class ObjectCache<X> implements Serializable {
+
+ private static final long serialVersionUID = 1L;
+
+ private final Map<String, CachedObject<X>> cache = new ConcurrentHashMap<String, CachedObject<X>>();
+
+ private class CachedObject<Y> {
+
+ public final String name;
+
+ private volatile Date date;
+
+ private volatile Y object;
+
+ CachedObject(String name) {
+ this.name = name;
+ date = new Date(0);
+ }
+
+ @Override
+ public String toString() {
+ return getClass().getSimpleName() + ": " + name;
+ }
+ }
+
+ public boolean hasCurrent(String name, Date date) {
+ return cache.containsKey(name) && cache.get(name).date.compareTo(date) == 0;
+ }
+
+ public Date getDate(String name) {
+ return cache.get(name).date;
+ }
+
+ public X getObject(String name) {
+ if (cache.containsKey(name)) {
+ return cache.get(name).object;
+ }
+ return null;
+ }
+
+ public void updateObject(String name, X object) {
+ this.updateObject(name, new Date(), object);
+ }
+
+ public void updateObject(String name, Date date, X object) {
+ CachedObject<X> obj;
+ if (cache.containsKey(name)) {
+ obj = cache.get(name);
+ } else {
+ obj = new CachedObject<X>(name);
+ cache.put(name, obj);
+ }
+ obj.date = date;
+ obj.object = object;
+ }
+
+ public Object remove(String name) {
+ if (cache.containsKey(name)) {
+ return cache.remove(name).object;
+ }
+ return null;
+ }
+
+ public int size() {
+ return cache.size();
+ }
+}
diff --git a/src/main/java/com/gitblit/utils/PatchFormatter.java b/src/main/java/com/gitblit/utils/PatchFormatter.java
new file mode 100644
index 00000000..90b3fb16
--- /dev/null
+++ b/src/main/java/com/gitblit/utils/PatchFormatter.java
@@ -0,0 +1,143 @@
+/*
+ * 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.IOException;
+import java.io.OutputStream;
+import java.text.MessageFormat;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.Map;
+
+import org.eclipse.jgit.diff.DiffEntry;
+import org.eclipse.jgit.diff.DiffFormatter;
+import org.eclipse.jgit.diff.RawText;
+import org.eclipse.jgit.revwalk.RevCommit;
+
+import com.gitblit.Constants;
+
+/**
+ * A diff formatter that outputs standard patch content.
+ *
+ * @author James Moger
+ *
+ */
+public class PatchFormatter extends DiffFormatter {
+
+ private final OutputStream os;
+
+ private Map<String, PatchTouple> changes = new HashMap<String, PatchTouple>();
+
+ private PatchTouple currentTouple;
+
+ public PatchFormatter(OutputStream os) {
+ super(os);
+ this.os = os;
+ }
+
+ public void format(DiffEntry entry) throws IOException {
+ currentTouple = new PatchTouple();
+ changes.put(entry.getNewPath(), currentTouple);
+ super.format(entry);
+ }
+
+ @Override
+ protected void writeLine(final char prefix, final RawText text, final int cur)
+ throws IOException {
+ switch (prefix) {
+ case '+':
+ currentTouple.insertions++;
+ break;
+ case '-':
+ currentTouple.deletions++;
+ break;
+ }
+ super.writeLine(prefix, text, cur);
+ }
+
+ public String getPatch(RevCommit commit) {
+ StringBuilder patch = new StringBuilder();
+ // hard-code the mon sep 17 2001 date string.
+ // I have no idea why that is there. it seems to be a constant.
+ patch.append("From " + commit.getName() + " Mon Sep 17 00:00:00 2001" + "\n");
+ patch.append("From: " + JGitUtils.getDisplayName(commit.getAuthorIdent()) + "\n");
+ patch.append("Date: "
+ + (new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss Z").format(new Date(commit
+ .getCommitTime() * 1000L))) + "\n");
+ patch.append("Subject: [PATCH] " + commit.getShortMessage() + "\n");
+ patch.append('\n');
+ patch.append("---");
+ int maxPathLen = 0;
+ int files = 0;
+ int insertions = 0;
+ int deletions = 0;
+ for (String path : changes.keySet()) {
+ if (path.length() > maxPathLen) {
+ maxPathLen = path.length();
+ }
+ PatchTouple touple = changes.get(path);
+ files++;
+ insertions += touple.insertions;
+ deletions += touple.deletions;
+ }
+ int columns = 60;
+ int total = insertions + deletions;
+ int unit = total / columns + (total % columns > 0 ? 1 : 0);
+ if (unit == 0) {
+ unit = 1;
+ }
+ for (String path : changes.keySet()) {
+ PatchTouple touple = changes.get(path);
+ patch.append("\n " + StringUtils.rightPad(path, maxPathLen, ' ') + " | "
+ + StringUtils.leftPad("" + touple.total(), 4, ' ') + " "
+ + touple.relativeScale(unit));
+ }
+ patch.append(MessageFormat.format(
+ "\n {0} files changed, {1} insertions(+), {2} deletions(-)\n\n", files, insertions,
+ deletions));
+ patch.append(os.toString());
+ patch.append("\n--\n");
+ patch.append(Constants.getGitBlitVersion());
+ return patch.toString();
+ }
+
+ /**
+ * Class that represents the number of insertions and deletions from a
+ * chunk.
+ */
+ private static class PatchTouple {
+ int insertions;
+ int deletions;
+
+ int total() {
+ return insertions + deletions;
+ }
+
+ String relativeScale(int unit) {
+ int plus = insertions / unit;
+ int minus = deletions / unit;
+ StringBuilder sb = new StringBuilder();
+ for (int i = 0; i < plus; i++) {
+ sb.append('+');
+ }
+ for (int i = 0; i < minus; i++) {
+ sb.append('-');
+ }
+ return sb.toString();
+ }
+ }
+}
diff --git a/src/main/java/com/gitblit/utils/PushLogUtils.java b/src/main/java/com/gitblit/utils/PushLogUtils.java
new file mode 100644
index 00000000..665533b3
--- /dev/null
+++ b/src/main/java/com/gitblit/utils/PushLogUtils.java
@@ -0,0 +1,344 @@
+/*
+ * Copyright 2013 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.IOException;
+import java.text.MessageFormat;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Date;
+import java.util.List;
+import java.util.Set;
+import java.util.TreeSet;
+
+import org.eclipse.jgit.api.errors.ConcurrentRefUpdateException;
+import org.eclipse.jgit.api.errors.JGitInternalException;
+import org.eclipse.jgit.dircache.DirCache;
+import org.eclipse.jgit.dircache.DirCacheBuilder;
+import org.eclipse.jgit.dircache.DirCacheEntry;
+import org.eclipse.jgit.internal.JGitText;
+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.PersonIdent;
+import org.eclipse.jgit.lib.RefUpdate;
+import org.eclipse.jgit.lib.RefUpdate.Result;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.revwalk.RevCommit;
+import org.eclipse.jgit.revwalk.RevWalk;
+import org.eclipse.jgit.transport.ReceiveCommand;
+import org.eclipse.jgit.treewalk.CanonicalTreeParser;
+import org.eclipse.jgit.treewalk.TreeWalk;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.gitblit.models.PathModel.PathChangeModel;
+import com.gitblit.models.PushLogEntry;
+import com.gitblit.models.RefModel;
+import com.gitblit.models.UserModel;
+
+/**
+ * Utility class for maintaining a pushlog within a git repository on an
+ * orphan branch.
+ *
+ * @author James Moger
+ *
+ */
+public class PushLogUtils {
+
+ public static final String GB_PUSHES = "refs/gitblit/pushes";
+
+ static final Logger LOGGER = LoggerFactory.getLogger(PushLogUtils.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<Object> parameters = new ArrayList<Object>();
+ 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 a RefModel for the gb-pushes branch in the repository. If the
+ * branch can not be found, null is returned.
+ *
+ * @param repository
+ * @return a refmodel for the gb-pushes branch or null
+ */
+ public static RefModel getPushLogBranch(Repository repository) {
+ List<RefModel> refs = JGitUtils.getRefs(repository, com.gitblit.Constants.R_GITBLIT);
+ for (RefModel ref : refs) {
+ if (ref.reference.getName().equals(GB_PUSHES)) {
+ return ref;
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Updates a push log.
+ *
+ * @param user
+ * @param repository
+ * @param commands
+ * @return true, if the update was successful
+ */
+ public static boolean updatePushLog(UserModel user, Repository repository,
+ Collection<ReceiveCommand> commands) {
+ RefModel pushlogBranch = getPushLogBranch(repository);
+ if (pushlogBranch == null) {
+ JGitUtils.createOrphanBranch(repository, GB_PUSHES, null);
+ }
+
+ boolean success = false;
+ String message = "push";
+
+ try {
+ ObjectId headId = repository.resolve(GB_PUSHES + "^{commit}");
+ ObjectInserter odi = repository.newObjectInserter();
+ try {
+ // Create the in-memory index of the push log entry
+ DirCache index = createIndex(repository, headId, commands);
+ ObjectId indexTreeId = index.writeTree(odi);
+
+ PersonIdent ident = new PersonIdent(user.getDisplayName(),
+ user.emailAddress == null ? user.username:user.emailAddress);
+
+ // Create a commit object
+ CommitBuilder commit = new CommitBuilder();
+ commit.setAuthor(ident);
+ commit.setCommitter(ident);
+ commit.setEncoding(Constants.CHARACTER_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(repository);
+ try {
+ RevCommit revCommit = revWalk.parseCommit(commitId);
+ RefUpdate ru = repository.updateRef(GB_PUSHES);
+ ru.setNewObjectId(commitId);
+ ru.setExpectedOldObjectId(headId);
+ ru.setRefLogMessage("commit: " + revCommit.getShortMessage(), false);
+ Result rc = ru.forceUpdate();
+ 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, GB_PUSHES, commitId.toString(),
+ rc));
+ }
+ } finally {
+ revWalk.release();
+ }
+ } finally {
+ odi.release();
+ }
+ } catch (Throwable t) {
+ error(t, repository, "Failed to commit pushlog entry to {0}");
+ }
+ return success;
+ }
+
+ /**
+ * Creates an in-memory index of the push log entry.
+ *
+ * @param repo
+ * @param headId
+ * @param commands
+ * @return an in-memory index
+ * @throws IOException
+ */
+ private static DirCache createIndex(Repository repo, ObjectId headId,
+ Collection<ReceiveCommand> commands) throws IOException {
+
+ DirCache inCoreIndex = DirCache.newInCore();
+ DirCacheBuilder dcBuilder = inCoreIndex.builder();
+ ObjectInserter inserter = repo.newObjectInserter();
+
+ long now = System.currentTimeMillis();
+ Set<String> ignorePaths = new TreeSet<String>();
+ try {
+ // add receive commands to the temporary index
+ for (ReceiveCommand command : commands) {
+ // use the ref names as the path names
+ String path = command.getRefName();
+ ignorePaths.add(path);
+
+ StringBuilder change = new StringBuilder();
+ change.append(command.getType().name()).append(' ');
+ switch (command.getType()) {
+ case CREATE:
+ change.append(ObjectId.zeroId().getName());
+ change.append(' ');
+ change.append(command.getNewId().getName());
+ break;
+ case UPDATE:
+ case UPDATE_NONFASTFORWARD:
+ change.append(command.getOldId().getName());
+ change.append(' ');
+ change.append(command.getNewId().getName());
+ break;
+ case DELETE:
+ change = null;
+ break;
+ }
+ if (change == null) {
+ // ref deleted
+ continue;
+ }
+ String content = change.toString();
+
+ // create an index entry for this attachment
+ final DirCacheEntry dcEntry = new DirCacheEntry(path);
+ dcEntry.setLength(content.length());
+ dcEntry.setLastModified(now);
+ dcEntry.setFileMode(FileMode.REGULAR_FILE);
+
+ // insert object
+ dcEntry.setObjectId(inserter.insert(Constants.OBJ_BLOB, content.getBytes("UTF-8")));
+
+ // add to temporary in-core index
+ dcBuilder.add(dcEntry);
+ }
+
+ // Traverse HEAD to add all other paths
+ TreeWalk treeWalk = new TreeWalk(repo);
+ int hIdx = -1;
+ if (headId != null)
+ hIdx = treeWalk.addTree(new RevWalk(repo).parseTree(headId));
+ treeWalk.setRecursive(true);
+
+ while (treeWalk.next()) {
+ String path = treeWalk.getPathString();
+ CanonicalTreeParser hTree = null;
+ if (hIdx != -1)
+ hTree = treeWalk.getTree(hIdx, CanonicalTreeParser.class);
+ if (!ignorePaths.contains(path)) {
+ // add entries from HEAD for all other paths
+ if (hTree != null) {
+ // create a new DirCacheEntry with data retrieved from
+ // HEAD
+ final DirCacheEntry dcEntry = new DirCacheEntry(path);
+ dcEntry.setObjectId(hTree.getEntryObjectId());
+ dcEntry.setFileMode(hTree.getEntryFileMode());
+
+ // add to temporary in-core index
+ dcBuilder.add(dcEntry);
+ }
+ }
+ }
+
+ // release the treewalk
+ treeWalk.release();
+
+ // finish temporary in-core index used for this commit
+ dcBuilder.finish();
+ } finally {
+ inserter.release();
+ }
+ return inCoreIndex;
+ }
+
+ public static List<PushLogEntry> getPushLog(String repositoryName, Repository repository) {
+ return getPushLog(repositoryName, repository, null, -1);
+ }
+
+ public static List<PushLogEntry> getPushLog(String repositoryName, Repository repository, int maxCount) {
+ return getPushLog(repositoryName, repository, null, maxCount);
+ }
+
+ public static List<PushLogEntry> getPushLog(String repositoryName, Repository repository, Date minimumDate) {
+ return getPushLog(repositoryName, repository, minimumDate, -1);
+ }
+
+ public static List<PushLogEntry> getPushLog(String repositoryName, Repository repository, Date minimumDate, int maxCount) {
+ List<PushLogEntry> list = new ArrayList<PushLogEntry>();
+ RefModel ref = getPushLogBranch(repository);
+ if (ref == null) {
+ return list;
+ }
+ List<RevCommit> pushes;
+ if (minimumDate == null) {
+ pushes = JGitUtils.getRevLog(repository, GB_PUSHES, 0, maxCount);
+ } else {
+ pushes = JGitUtils.getRevLog(repository, GB_PUSHES, minimumDate);
+ }
+ for (RevCommit push : pushes) {
+ if (push.getAuthorIdent().getName().equalsIgnoreCase("gitblit")) {
+ // skip gitblit/internal commits
+ continue;
+ }
+ Date date = push.getAuthorIdent().getWhen();
+ UserModel user = new UserModel(push.getAuthorIdent().getEmailAddress());
+ user.displayName = push.getAuthorIdent().getName();
+ PushLogEntry log = new PushLogEntry(repositoryName, date, user);
+ list.add(log);
+ List<PathChangeModel> changedRefs = JGitUtils.getFilesInCommit(repository, push);
+ for (PathChangeModel change : changedRefs) {
+ switch (change.changeType) {
+ case DELETE:
+ log.updateRef(change.path, ReceiveCommand.Type.DELETE);
+ break;
+ case ADD:
+ log.updateRef(change.path, ReceiveCommand.Type.CREATE);
+ default:
+ String content = JGitUtils.getStringContent(repository, push.getTree(), change.path);
+ String [] fields = content.split(" ");
+ log.updateRef(change.path, ReceiveCommand.Type.valueOf(fields[0]));
+ String oldId = fields[1];
+ String newId = fields[2];
+ List<RevCommit> pushedCommits = JGitUtils.getRevLog(repository, oldId, newId);
+ for (RevCommit pushedCommit : pushedCommits) {
+ log.addCommit(change.path, pushedCommit);
+ }
+ }
+ }
+ }
+ Collections.sort(list);
+ return list;
+ }
+}
diff --git a/src/main/java/com/gitblit/utils/RpcUtils.java b/src/main/java/com/gitblit/utils/RpcUtils.java
new file mode 100644
index 00000000..ed23dab6
--- /dev/null
+++ b/src/main/java/com/gitblit/utils/RpcUtils.java
@@ -0,0 +1,637 @@
+/*
+ * 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.IOException;
+import java.lang.reflect.Type;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+
+import com.gitblit.Constants;
+import com.gitblit.Constants.RpcRequest;
+import com.gitblit.GitBlitException.UnknownRequestException;
+import com.gitblit.models.RegistrantAccessPermission;
+import com.gitblit.models.FederationModel;
+import com.gitblit.models.FederationProposal;
+import com.gitblit.models.FederationSet;
+import com.gitblit.models.FeedModel;
+import com.gitblit.models.RepositoryModel;
+import com.gitblit.models.ServerSettings;
+import com.gitblit.models.ServerStatus;
+import com.gitblit.models.TeamModel;
+import com.gitblit.models.UserModel;
+import com.google.gson.reflect.TypeToken;
+
+/**
+ * Utility methods for rpc calls.
+ *
+ * @author James Moger
+ *
+ */
+public class RpcUtils {
+
+ public static final Type NAMES_TYPE = new TypeToken<Collection<String>>() {
+ }.getType();
+
+ public static final Type SETTINGS_TYPE = new TypeToken<Map<String, String>>() {
+ }.getType();
+
+ private static final Type REPOSITORIES_TYPE = new TypeToken<Map<String, RepositoryModel>>() {
+ }.getType();
+
+ private static final Type USERS_TYPE = new TypeToken<Collection<UserModel>>() {
+ }.getType();
+
+ private static final Type TEAMS_TYPE = new TypeToken<Collection<TeamModel>>() {
+ }.getType();
+
+ private static final Type REGISTRATIONS_TYPE = new TypeToken<Collection<FederationModel>>() {
+ }.getType();
+
+ private static final Type PROPOSALS_TYPE = new TypeToken<Collection<FederationProposal>>() {
+ }.getType();
+
+ private static final Type SETS_TYPE = new TypeToken<Collection<FederationSet>>() {
+ }.getType();
+
+ private static final Type BRANCHES_TYPE = new TypeToken<Map<String, Collection<String>>>() {
+ }.getType();
+
+ public static final Type REGISTRANT_PERMISSIONS_TYPE = new TypeToken<Collection<RegistrantAccessPermission>>() {
+ }.getType();
+
+ /**
+ *
+ * @param remoteURL
+ * the url of the remote gitblit instance
+ * @param req
+ * the rpc request type
+ * @return
+ */
+ public static String asLink(String remoteURL, RpcRequest req) {
+ return asLink(remoteURL, req, null);
+ }
+
+ /**
+ *
+ * @param remoteURL
+ * the url of the remote gitblit instance
+ * @param req
+ * the rpc request type
+ * @param name
+ * the name of the actionable object
+ * @return
+ */
+ public static String asLink(String remoteURL, RpcRequest req, String name) {
+ if (remoteURL.length() > 0 && remoteURL.charAt(remoteURL.length() - 1) == '/') {
+ remoteURL = remoteURL.substring(0, remoteURL.length() - 1);
+ }
+ if (req == null) {
+ req = RpcRequest.LIST_REPOSITORIES;
+ }
+ return remoteURL + Constants.RPC_PATH + "?req=" + req.name().toLowerCase()
+ + (name == null ? "" : ("&name=" + StringUtils.encodeURL(name)));
+ }
+
+ /**
+ * Returns the version of the RPC protocol on the server.
+ *
+ * @param serverUrl
+ * @param account
+ * @param password
+ * @return the protocol version
+ * @throws IOException
+ */
+ public static int getProtocolVersion(String serverUrl, String account, char[] password)
+ throws IOException {
+ String url = asLink(serverUrl, RpcRequest.GET_PROTOCOL);
+ int protocol = 1;
+ try {
+ protocol = JsonUtils.retrieveJson(url, Integer.class, account, password);
+ } catch (UnknownRequestException e) {
+ // v0.7.0 (protocol 1) did not have this request type
+ }
+ return protocol;
+ }
+
+ /**
+ * Retrieves a map of the repositories at the remote gitblit instance keyed
+ * by the repository clone url.
+ *
+ * @param serverUrl
+ * @param account
+ * @param password
+ * @return a map of cloneable repositories
+ * @throws IOException
+ */
+ public static Map<String, RepositoryModel> getRepositories(String serverUrl, String account,
+ char[] password) throws IOException {
+ String url = asLink(serverUrl, RpcRequest.LIST_REPOSITORIES);
+ Map<String, RepositoryModel> models = JsonUtils.retrieveJson(url, REPOSITORIES_TYPE,
+ account, password);
+ return models;
+ }
+
+ /**
+ * Tries to pull the gitblit user accounts from the remote gitblit instance.
+ *
+ * @param serverUrl
+ * @param account
+ * @param password
+ * @return a collection of UserModel objects
+ * @throws IOException
+ */
+ public static List<UserModel> getUsers(String serverUrl, String account, char[] password)
+ throws IOException {
+ String url = asLink(serverUrl, RpcRequest.LIST_USERS);
+ Collection<UserModel> models = JsonUtils.retrieveJson(url, USERS_TYPE, account, password);
+ List<UserModel> list = new ArrayList<UserModel>(models);
+ return list;
+ }
+
+ /**
+ * Tries to pull the gitblit team definitions from the remote gitblit
+ * instance.
+ *
+ * @param serverUrl
+ * @param account
+ * @param password
+ * @return a collection of UserModel objects
+ * @throws IOException
+ */
+ public static List<TeamModel> getTeams(String serverUrl, String account, char[] password)
+ throws IOException {
+ String url = asLink(serverUrl, RpcRequest.LIST_TEAMS);
+ Collection<TeamModel> models = JsonUtils.retrieveJson(url, TEAMS_TYPE, account, password);
+ List<TeamModel> list = new ArrayList<TeamModel>(models);
+ return list;
+ }
+
+ /**
+ * Create a repository on the Gitblit server.
+ *
+ * @param repository
+ * @param serverUrl
+ * @param account
+ * @param password
+ * @return true if the action succeeded
+ * @throws IOException
+ */
+ public static boolean createRepository(RepositoryModel repository, String serverUrl,
+ String account, char[] password) throws IOException {
+ // ensure repository name ends with .git
+ if (!repository.name.endsWith(".git")) {
+ repository.name += ".git";
+ }
+ return doAction(RpcRequest.CREATE_REPOSITORY, null, repository, serverUrl, account,
+ password);
+
+ }
+
+ /**
+ * Send a revised version of the repository model to the Gitblit server.
+ *
+ * @param repository
+ * @param serverUrl
+ * @param account
+ * @param password
+ * @return true if the action succeeded
+ * @throws IOException
+ */
+ public static boolean updateRepository(String repositoryName, RepositoryModel repository,
+ String serverUrl, String account, char[] password) throws IOException {
+ return doAction(RpcRequest.EDIT_REPOSITORY, repositoryName, repository, serverUrl, account,
+ password);
+ }
+
+ /**
+ * Delete a repository from the Gitblit server.
+ *
+ * @param repository
+ * @param serverUrl
+ * @param account
+ * @param password
+ * @return true if the action succeeded
+ * @throws IOException
+ */
+ public static boolean deleteRepository(RepositoryModel repository, String serverUrl,
+ String account, char[] password) throws IOException {
+ return doAction(RpcRequest.DELETE_REPOSITORY, null, repository, serverUrl, account,
+ password);
+
+ }
+
+ /**
+ * Clears the repository cache on the Gitblit server.
+ *
+ * @param serverUrl
+ * @param account
+ * @param password
+ * @return true if the action succeeded
+ * @throws IOException
+ */
+ public static boolean clearRepositoryCache(String serverUrl, String account,
+ char[] password) throws IOException {
+ return doAction(RpcRequest.CLEAR_REPOSITORY_CACHE, null, null, serverUrl, account,
+ password);
+ }
+
+ /**
+ * Create a user on the Gitblit server.
+ *
+ * @param user
+ * @param serverUrl
+ * @param account
+ * @param password
+ * @return true if the action succeeded
+ * @throws IOException
+ */
+ public static boolean createUser(UserModel user, String serverUrl, String account,
+ char[] password) throws IOException {
+ return doAction(RpcRequest.CREATE_USER, null, user, serverUrl, account, password);
+
+ }
+
+ /**
+ * Send a revised version of the user model to the Gitblit server.
+ *
+ * @param user
+ * @param serverUrl
+ * @param account
+ * @param password
+ * @return true if the action succeeded
+ * @throws IOException
+ */
+ public static boolean updateUser(String username, UserModel user, String serverUrl,
+ String account, char[] password) throws IOException {
+ return doAction(RpcRequest.EDIT_USER, username, user, serverUrl, account, password);
+
+ }
+
+ /**
+ * Deletes a user from the Gitblit server.
+ *
+ * @param user
+ * @param serverUrl
+ * @param account
+ * @param password
+ * @return true if the action succeeded
+ * @throws IOException
+ */
+ public static boolean deleteUser(UserModel user, String serverUrl, String account,
+ char[] password) throws IOException {
+ return doAction(RpcRequest.DELETE_USER, null, user, serverUrl, account, password);
+ }
+
+ /**
+ * Create a team on the Gitblit server.
+ *
+ * @param team
+ * @param serverUrl
+ * @param account
+ * @param password
+ * @return true if the action succeeded
+ * @throws IOException
+ */
+ public static boolean createTeam(TeamModel team, String serverUrl, String account,
+ char[] password) throws IOException {
+ return doAction(RpcRequest.CREATE_TEAM, null, team, serverUrl, account, password);
+
+ }
+
+ /**
+ * Send a revised version of the team model to the Gitblit server.
+ *
+ * @param team
+ * @param serverUrl
+ * @param account
+ * @param password
+ * @return true if the action succeeded
+ * @throws IOException
+ */
+ public static boolean updateTeam(String teamname, TeamModel team, String serverUrl,
+ String account, char[] password) throws IOException {
+ return doAction(RpcRequest.EDIT_TEAM, teamname, team, serverUrl, account, password);
+
+ }
+
+ /**
+ * Deletes a team from the Gitblit server.
+ *
+ * @param team
+ * @param serverUrl
+ * @param account
+ * @param password
+ * @return true if the action succeeded
+ * @throws IOException
+ */
+ public static boolean deleteTeam(TeamModel team, String serverUrl, String account,
+ char[] password) throws IOException {
+ return doAction(RpcRequest.DELETE_TEAM, null, team, serverUrl, account, password);
+ }
+
+ /**
+ * Retrieves the list of users that can access the specified repository.
+ *
+ * @param repository
+ * @param serverUrl
+ * @param account
+ * @param password
+ * @return list of members
+ * @throws IOException
+ */
+ public static List<String> getRepositoryMembers(RepositoryModel repository, String serverUrl,
+ String account, char[] password) throws IOException {
+ String url = asLink(serverUrl, RpcRequest.LIST_REPOSITORY_MEMBERS, repository.name);
+ Collection<String> list = JsonUtils.retrieveJson(url, NAMES_TYPE, account, password);
+ return new ArrayList<String>(list);
+ }
+
+ /**
+ * Retrieves the list of user access permissions for the specified repository.
+ *
+ * @param repository
+ * @param serverUrl
+ * @param account
+ * @param password
+ * @return list of User-AccessPermission tuples
+ * @throws IOException
+ */
+ public static List<RegistrantAccessPermission> getRepositoryMemberPermissions(RepositoryModel repository,
+ String serverUrl, String account, char [] password) throws IOException {
+ String url = asLink(serverUrl, RpcRequest.LIST_REPOSITORY_MEMBER_PERMISSIONS, repository.name);
+ Collection<RegistrantAccessPermission> list = JsonUtils.retrieveJson(url, REGISTRANT_PERMISSIONS_TYPE, account, password);
+ return new ArrayList<RegistrantAccessPermission>(list);
+ }
+
+ /**
+ * Sets the repository user access permissions
+ *
+ * @param repository
+ * @param permissions
+ * @param serverUrl
+ * @param account
+ * @param password
+ * @return true if the action succeeded
+ * @throws IOException
+ */
+ public static boolean setRepositoryMemberPermissions(RepositoryModel repository,
+ List<RegistrantAccessPermission> permissions, String serverUrl, String account, char[] password)
+ throws IOException {
+ return doAction(RpcRequest.SET_REPOSITORY_MEMBER_PERMISSIONS, repository.name, permissions, serverUrl,
+ account, password);
+ }
+
+ /**
+ * Retrieves the list of teams that can access the specified repository.
+ *
+ * @param repository
+ * @param serverUrl
+ * @param account
+ * @param password
+ * @return list of teams
+ * @throws IOException
+ */
+ public static List<String> getRepositoryTeams(RepositoryModel repository, String serverUrl,
+ String account, char[] password) throws IOException {
+ String url = asLink(serverUrl, RpcRequest.LIST_REPOSITORY_TEAMS, repository.name);
+ Collection<String> list = JsonUtils.retrieveJson(url, NAMES_TYPE, account, password);
+ return new ArrayList<String>(list);
+ }
+
+ /**
+ * Retrieves the list of team access permissions for the specified repository.
+ *
+ * @param repository
+ * @param serverUrl
+ * @param account
+ * @param password
+ * @return list of Team-AccessPermission tuples
+ * @throws IOException
+ */
+ public static List<RegistrantAccessPermission> getRepositoryTeamPermissions(RepositoryModel repository,
+ String serverUrl, String account, char [] password) throws IOException {
+ String url = asLink(serverUrl, RpcRequest.LIST_REPOSITORY_TEAM_PERMISSIONS, repository.name);
+ Collection<RegistrantAccessPermission> list = JsonUtils.retrieveJson(url, REGISTRANT_PERMISSIONS_TYPE, account, password);
+ return new ArrayList<RegistrantAccessPermission>(list);
+ }
+
+ /**
+ * Sets the repository team access permissions
+ *
+ * @param repository
+ * @param permissions
+ * @param serverUrl
+ * @param account
+ * @param password
+ * @return true if the action succeeded
+ * @throws IOException
+ */
+ public static boolean setRepositoryTeamPermissions(RepositoryModel repository,
+ List<RegistrantAccessPermission> permissions, String serverUrl, String account, char[] password)
+ throws IOException {
+ return doAction(RpcRequest.SET_REPOSITORY_TEAM_PERMISSIONS, repository.name, permissions, serverUrl,
+ account, password);
+ }
+
+ /**
+ * Retrieves the list of federation registrations. These are the list of
+ * registrations that this Gitblit instance is pulling from.
+ *
+ * @param serverUrl
+ * @param account
+ * @param password
+ * @return a collection of FederationRegistration objects
+ * @throws IOException
+ */
+ public static List<FederationModel> getFederationRegistrations(String serverUrl,
+ String account, char[] password) throws IOException {
+ String url = asLink(serverUrl, RpcRequest.LIST_FEDERATION_REGISTRATIONS);
+ Collection<FederationModel> registrations = JsonUtils.retrieveJson(url, REGISTRATIONS_TYPE,
+ account, password);
+ List<FederationModel> list = new ArrayList<FederationModel>(registrations);
+ return list;
+ }
+
+ /**
+ * Retrieves the list of federation result registrations. These are the
+ * results reported back to this Gitblit instance from a federation client.
+ *
+ * @param serverUrl
+ * @param account
+ * @param password
+ * @return a collection of FederationRegistration objects
+ * @throws IOException
+ */
+ public static List<FederationModel> getFederationResultRegistrations(String serverUrl,
+ String account, char[] password) throws IOException {
+ String url = asLink(serverUrl, RpcRequest.LIST_FEDERATION_RESULTS);
+ Collection<FederationModel> registrations = JsonUtils.retrieveJson(url, REGISTRATIONS_TYPE,
+ account, password);
+ List<FederationModel> list = new ArrayList<FederationModel>(registrations);
+ return list;
+ }
+
+ /**
+ * Retrieves the list of federation proposals.
+ *
+ * @param serverUrl
+ * @param account
+ * @param password
+ * @return a collection of FederationProposal objects
+ * @throws IOException
+ */
+ public static List<FederationProposal> getFederationProposals(String serverUrl, String account,
+ char[] password) throws IOException {
+ String url = asLink(serverUrl, RpcRequest.LIST_FEDERATION_PROPOSALS);
+ Collection<FederationProposal> proposals = JsonUtils.retrieveJson(url, PROPOSALS_TYPE,
+ account, password);
+ List<FederationProposal> list = new ArrayList<FederationProposal>(proposals);
+ return list;
+ }
+
+ /**
+ * Retrieves the list of federation repository sets.
+ *
+ * @param serverUrl
+ * @param account
+ * @param password
+ * @return a collection of FederationSet objects
+ * @throws IOException
+ */
+ public static List<FederationSet> getFederationSets(String serverUrl, String account,
+ char[] password) throws IOException {
+ String url = asLink(serverUrl, RpcRequest.LIST_FEDERATION_SETS);
+ Collection<FederationSet> sets = JsonUtils.retrieveJson(url, SETS_TYPE, account, password);
+ List<FederationSet> list = new ArrayList<FederationSet>(sets);
+ return list;
+ }
+
+ /**
+ * Retrieves the settings of the Gitblit server.
+ *
+ * @param serverUrl
+ * @param account
+ * @param password
+ * @return an Settings object
+ * @throws IOException
+ */
+ public static ServerSettings getSettings(String serverUrl, String account, char[] password)
+ throws IOException {
+ String url = asLink(serverUrl, RpcRequest.LIST_SETTINGS);
+ ServerSettings settings = JsonUtils.retrieveJson(url, ServerSettings.class, account,
+ password);
+ return settings;
+ }
+
+ /**
+ * Update the settings on the Gitblit server.
+ *
+ * @param settings
+ * the settings to update
+ * @param serverUrl
+ * @param account
+ * @param password
+ * @return true if the action succeeded
+ * @throws IOException
+ */
+ public static boolean updateSettings(Map<String, String> settings, String serverUrl,
+ String account, char[] password) throws IOException {
+ return doAction(RpcRequest.EDIT_SETTINGS, null, settings, serverUrl, account, password);
+
+ }
+
+ /**
+ * Retrieves the server status object.
+ *
+ * @param serverUrl
+ * @param account
+ * @param password
+ * @return an ServerStatus object
+ * @throws IOException
+ */
+ public static ServerStatus getStatus(String serverUrl, String account, char[] password)
+ throws IOException {
+ String url = asLink(serverUrl, RpcRequest.LIST_STATUS);
+ ServerStatus status = JsonUtils.retrieveJson(url, ServerStatus.class, account, password);
+ return status;
+ }
+
+ /**
+ * Retrieves a map of local branches in the Gitblit server keyed by
+ * repository.
+ *
+ * @param serverUrl
+ * @param account
+ * @param password
+ * @return
+ * @throws IOException
+ */
+ public static Map<String, Collection<String>> getBranches(String serverUrl, String account,
+ char[] password) throws IOException {
+ String url = asLink(serverUrl, RpcRequest.LIST_BRANCHES);
+ Map<String, Collection<String>> branches = JsonUtils.retrieveJson(url, BRANCHES_TYPE,
+ account, password);
+ return branches;
+ }
+
+ /**
+ * Retrieves a list of available branch feeds in the Gitblit server.
+ *
+ * @param serverUrl
+ * @param account
+ * @param password
+ * @return
+ * @throws IOException
+ */
+ public static List<FeedModel> getBranchFeeds(String serverUrl, String account, char[] password)
+ throws IOException {
+ List<FeedModel> feeds = new ArrayList<FeedModel>();
+ Map<String, Collection<String>> allBranches = getBranches(serverUrl, account, password);
+ for (Map.Entry<String, Collection<String>> entry : allBranches.entrySet()) {
+ for (String branch : entry.getValue()) {
+ FeedModel feed = new FeedModel();
+ feed.repository = entry.getKey();
+ feed.branch = branch;
+ feeds.add(feed);
+ }
+ }
+ return feeds;
+ }
+
+ /**
+ * Do the specified administrative action on the Gitblit server.
+ *
+ * @param request
+ * @param name
+ * the name of the object (may be null)
+ * @param object
+ * @param serverUrl
+ * @param account
+ * @param password
+ * @return true if the action succeeded
+ * @throws IOException
+ */
+ protected static boolean doAction(RpcRequest request, String name, Object object,
+ String serverUrl, String account, char[] password) throws IOException {
+ String url = asLink(serverUrl, request, name);
+ String json = JsonUtils.toJsonString(object);
+ int resultCode = JsonUtils.sendJsonString(url, json, account, password);
+ return resultCode == 200;
+ }
+}
diff --git a/src/main/java/com/gitblit/utils/StringUtils.java b/src/main/java/com/gitblit/utils/StringUtils.java
new file mode 100644
index 00000000..86823db5
--- /dev/null
+++ b/src/main/java/com/gitblit/utils/StringUtils.java
@@ -0,0 +1,736 @@
+/*
+ * 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.ByteArrayOutputStream;
+import java.io.UnsupportedEncodingException;
+import java.nio.ByteBuffer;
+import java.nio.CharBuffer;
+import java.nio.charset.CharacterCodingException;
+import java.nio.charset.Charset;
+import java.nio.charset.CharsetDecoder;
+import java.nio.charset.IllegalCharsetNameException;
+import java.nio.charset.UnsupportedCharsetException;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Set;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+import java.util.regex.PatternSyntaxException;
+
+/**
+ * Utility class of string functions.
+ *
+ * @author James Moger
+ *
+ */
+public class StringUtils {
+
+ public static final String MD5_TYPE = "MD5:";
+
+ public static final String COMBINED_MD5_TYPE = "CMD5:";
+
+ /**
+ * Returns true if the string is null or empty.
+ *
+ * @param value
+ * @return true if string is null or empty
+ */
+ public static boolean isEmpty(String value) {
+ return value == null || value.trim().length() == 0;
+ }
+
+ /**
+ * Replaces carriage returns and line feeds with html line breaks.
+ *
+ * @param string
+ * @return plain text with html line breaks
+ */
+ public static String breakLinesForHtml(String string) {
+ return string.replace("\r\n", "<br/>").replace("\r", "<br/>").replace("\n", "<br/>");
+ }
+
+ /**
+ * Prepare text for html presentation. Replace sensitive characters with
+ * html entities.
+ *
+ * @param inStr
+ * @param changeSpace
+ * @return plain text escaped for html
+ */
+ public static String escapeForHtml(String inStr, boolean changeSpace) {
+ StringBuilder retStr = new StringBuilder();
+ int i = 0;
+ while (i < inStr.length()) {
+ if (inStr.charAt(i) == '&') {
+ retStr.append("&amp;");
+ } else if (inStr.charAt(i) == '<') {
+ retStr.append("&lt;");
+ } else if (inStr.charAt(i) == '>') {
+ retStr.append("&gt;");
+ } else if (inStr.charAt(i) == '\"') {
+ retStr.append("&quot;");
+ } else if (changeSpace && inStr.charAt(i) == ' ') {
+ retStr.append("&nbsp;");
+ } else if (changeSpace && inStr.charAt(i) == '\t') {
+ retStr.append(" &nbsp; &nbsp;");
+ } else {
+ retStr.append(inStr.charAt(i));
+ }
+ i++;
+ }
+ return retStr.toString();
+ }
+
+ /**
+ * Decode html entities back into plain text characters.
+ *
+ * @param inStr
+ * @return returns plain text from html
+ */
+ public static String decodeFromHtml(String inStr) {
+ return inStr.replace("&amp;", "&").replace("&lt;", "<").replace("&gt;", ">")
+ .replace("&quot;", "\"").replace("&nbsp;", " ");
+ }
+
+ /**
+ * Encodes a url parameter by escaping troublesome characters.
+ *
+ * @param inStr
+ * @return properly escaped url
+ */
+ public static String encodeURL(String inStr) {
+ StringBuilder retStr = new StringBuilder();
+ int i = 0;
+ while (i < inStr.length()) {
+ if (inStr.charAt(i) == '/') {
+ retStr.append("%2F");
+ } else if (inStr.charAt(i) == ' ') {
+ retStr.append("%20");
+ } else {
+ retStr.append(inStr.charAt(i));
+ }
+ i++;
+ }
+ return retStr.toString();
+ }
+
+ /**
+ * Flatten the list of strings into a single string with a space separator.
+ *
+ * @param values
+ * @return flattened list
+ */
+ public static String flattenStrings(Collection<String> values) {
+ return flattenStrings(values, " ");
+ }
+
+ /**
+ * Flatten the list of strings into a single string with the specified
+ * separator.
+ *
+ * @param values
+ * @param separator
+ * @return flattened list
+ */
+ public static String flattenStrings(Collection<String> values, String separator) {
+ StringBuilder sb = new StringBuilder();
+ for (String value : values) {
+ sb.append(value).append(separator);
+ }
+ if (sb.length() > 0) {
+ // truncate trailing separator
+ sb.setLength(sb.length() - separator.length());
+ }
+ return sb.toString().trim();
+ }
+
+ /**
+ * Returns a string trimmed to a maximum length with trailing ellipses. If
+ * the string length is shorter than the max, the original string is
+ * returned.
+ *
+ * @param value
+ * @param max
+ * @return trimmed string
+ */
+ public static String trimString(String value, int max) {
+ if (value.length() <= max) {
+ return value;
+ }
+ return value.substring(0, max - 3) + "...";
+ }
+
+ /**
+ * Left pad a string with the specified character, if the string length is
+ * less than the specified length.
+ *
+ * @param input
+ * @param length
+ * @param pad
+ * @return left-padded string
+ */
+ public static String leftPad(String input, int length, char pad) {
+ if (input.length() < length) {
+ StringBuilder sb = new StringBuilder();
+ for (int i = 0, len = length - input.length(); i < len; i++) {
+ sb.append(pad);
+ }
+ sb.append(input);
+ return sb.toString();
+ }
+ return input;
+ }
+
+ /**
+ * Right pad a string with the specified character, if the string length is
+ * less then the specified length.
+ *
+ * @param input
+ * @param length
+ * @param pad
+ * @return right-padded string
+ */
+ public static String rightPad(String input, int length, char pad) {
+ if (input.length() < length) {
+ StringBuilder sb = new StringBuilder();
+ sb.append(input);
+ for (int i = 0, len = length - input.length(); i < len; i++) {
+ sb.append(pad);
+ }
+ return sb.toString();
+ }
+ return input;
+ }
+
+ /**
+ * Calculates the SHA1 of the string.
+ *
+ * @param text
+ * @return sha1 of the string
+ */
+ public static String getSHA1(String text) {
+ try {
+ byte[] bytes = text.getBytes("iso-8859-1");
+ return getSHA1(bytes);
+ } catch (UnsupportedEncodingException u) {
+ throw new RuntimeException(u);
+ }
+ }
+
+ /**
+ * Calculates the SHA1 of the byte array.
+ *
+ * @param bytes
+ * @return sha1 of the byte array
+ */
+ public static String getSHA1(byte[] bytes) {
+ try {
+ MessageDigest md = MessageDigest.getInstance("SHA-1");
+ md.update(bytes, 0, bytes.length);
+ byte[] digest = md.digest();
+ return toHex(digest);
+ } catch (NoSuchAlgorithmException t) {
+ throw new RuntimeException(t);
+ }
+ }
+
+ /**
+ * Calculates the MD5 of the string.
+ *
+ * @param string
+ * @return md5 of the string
+ */
+ public static String getMD5(String string) {
+ try {
+ return getMD5(string.getBytes("iso-8859-1"));
+ } catch (UnsupportedEncodingException u) {
+ throw new RuntimeException(u);
+ }
+ }
+
+ /**
+ * Calculates the MD5 of the string.
+ *
+ * @param string
+ * @return md5 of the string
+ */
+ public static String getMD5(byte [] bytes) {
+ try {
+ MessageDigest md = MessageDigest.getInstance("MD5");
+ md.reset();
+ md.update(bytes);
+ byte[] digest = md.digest();
+ return toHex(digest);
+ } catch (NoSuchAlgorithmException t) {
+ throw new RuntimeException(t);
+ }
+ }
+
+ /**
+ * Returns the hex representation of the byte array.
+ *
+ * @param bytes
+ * @return byte array as hex string
+ */
+ private static String toHex(byte[] bytes) {
+ StringBuilder sb = new StringBuilder(bytes.length * 2);
+ for (int i = 0; i < bytes.length; i++) {
+ if (((int) bytes[i] & 0xff) < 0x10) {
+ sb.append('0');
+ }
+ sb.append(Long.toString((int) bytes[i] & 0xff, 16));
+ }
+ return sb.toString();
+ }
+
+ /**
+ * Returns the root path of the specified path. Returns a blank string if
+ * there is no root path.
+ *
+ * @param path
+ * @return root path or blank
+ */
+ public static String getRootPath(String path) {
+ if (path.indexOf('/') > -1) {
+ return path.substring(0, path.lastIndexOf('/'));
+ }
+ return "";
+ }
+
+ /**
+ * Returns the path remainder after subtracting the basePath from the
+ * fullPath.
+ *
+ * @param basePath
+ * @param fullPath
+ * @return the relative path
+ */
+ public static String getRelativePath(String basePath, String fullPath) {
+ String bp = basePath.replace('\\', '/').toLowerCase();
+ String fp = fullPath.replace('\\', '/').toLowerCase();
+ if (fp.startsWith(bp)) {
+ String relativePath = fullPath.substring(basePath.length()).replace('\\', '/');
+ if (relativePath.charAt(0) == '/') {
+ relativePath = relativePath.substring(1);
+ }
+ return relativePath;
+ }
+ return fullPath;
+ }
+
+ /**
+ * Splits the space-separated string into a list of strings.
+ *
+ * @param value
+ * @return list of strings
+ */
+ public static List<String> getStringsFromValue(String value) {
+ return getStringsFromValue(value, " ");
+ }
+
+ /**
+ * Splits the string into a list of string by the specified separator.
+ *
+ * @param value
+ * @param separator
+ * @return list of strings
+ */
+ public static List<String> getStringsFromValue(String value, String separator) {
+ List<String> strings = new ArrayList<String>();
+ try {
+ String[] chunks = value.split(separator + "(?=([^\"]*\"[^\"]*\")*[^\"]*$)");
+ for (String chunk : chunks) {
+ chunk = chunk.trim();
+ if (chunk.length() > 0) {
+ if (chunk.charAt(0) == '"' && chunk.charAt(chunk.length() - 1) == '"') {
+ // strip double quotes
+ chunk = chunk.substring(1, chunk.length() - 1).trim();
+ }
+ strings.add(chunk);
+ }
+ }
+ } catch (PatternSyntaxException e) {
+ throw new RuntimeException(e);
+ }
+ return strings;
+ }
+
+ /**
+ * Validates that a name is composed of letters, digits, or limited other
+ * characters.
+ *
+ * @param name
+ * @return the first invalid character found or null if string is acceptable
+ */
+ public static Character findInvalidCharacter(String name) {
+ char[] validChars = { '/', '.', '_', '-', '~' };
+ for (char c : name.toCharArray()) {
+ if (!Character.isLetterOrDigit(c)) {
+ boolean ok = false;
+ for (char vc : validChars) {
+ ok |= c == vc;
+ }
+ if (!ok) {
+ return c;
+ }
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Simple fuzzy string comparison. This is a case-insensitive check. A
+ * single wildcard * value is supported.
+ *
+ * @param value
+ * @param pattern
+ * @return true if the value matches the pattern
+ */
+ public static boolean fuzzyMatch(String value, String pattern) {
+ if (value.equalsIgnoreCase(pattern)) {
+ return true;
+ }
+ if (pattern.contains("*")) {
+ boolean prefixMatches = false;
+ boolean suffixMatches = false;
+
+ int wildcard = pattern.indexOf('*');
+ String prefix = pattern.substring(0, wildcard).toLowerCase();
+ prefixMatches = value.toLowerCase().startsWith(prefix);
+
+ if (pattern.length() > (wildcard + 1)) {
+ String suffix = pattern.substring(wildcard + 1).toLowerCase();
+ suffixMatches = value.toLowerCase().endsWith(suffix);
+ return prefixMatches && suffixMatches;
+ }
+ return prefixMatches || suffixMatches;
+ }
+ return false;
+ }
+
+ /**
+ * Compare two repository names for proper group sorting.
+ *
+ * @param r1
+ * @param r2
+ * @return
+ */
+ public static int compareRepositoryNames(String r1, String r2) {
+ // sort root repositories first, alphabetically
+ // then sort grouped repositories, alphabetically
+ r1 = r1.toLowerCase();
+ r2 = r2.toLowerCase();
+ int s1 = r1.indexOf('/');
+ int s2 = r2.indexOf('/');
+ if (s1 == -1 && s2 == -1) {
+ // neither grouped
+ return r1.compareTo(r2);
+ } else if (s1 > -1 && s2 > -1) {
+ // both grouped
+ return r1.compareTo(r2);
+ } else if (s1 == -1) {
+ return -1;
+ } else if (s2 == -1) {
+ return 1;
+ }
+ return 0;
+ }
+
+ /**
+ * Sort grouped repository names.
+ *
+ * @param list
+ */
+ public static void sortRepositorynames(List<String> list) {
+ Collections.sort(list, new Comparator<String>() {
+ @Override
+ public int compare(String o1, String o2) {
+ return compareRepositoryNames(o1, o2);
+ }
+ });
+ }
+
+ public static String getColor(String value) {
+ int cs = 0;
+ for (char c : getMD5(value.toLowerCase()).toCharArray()) {
+ cs += c;
+ }
+ int n = (cs % 360);
+ float hue = ((float) n) / 360;
+ return hsvToRgb(hue, 0.90f, 0.65f);
+ }
+
+ public static String hsvToRgb(float hue, float saturation, float value) {
+ int h = (int) (hue * 6);
+ float f = hue * 6 - h;
+ float p = value * (1 - saturation);
+ float q = value * (1 - f * saturation);
+ float t = value * (1 - (1 - f) * saturation);
+
+ switch (h) {
+ case 0:
+ return rgbToString(value, t, p);
+ case 1:
+ return rgbToString(q, value, p);
+ case 2:
+ return rgbToString(p, value, t);
+ case 3:
+ return rgbToString(p, q, value);
+ case 4:
+ return rgbToString(t, p, value);
+ case 5:
+ return rgbToString(value, p, q);
+ default:
+ throw new RuntimeException(
+ "Something went wrong when converting from HSV to RGB. Input was " + hue + ", "
+ + saturation + ", " + value);
+ }
+ }
+
+ public static String rgbToString(float r, float g, float b) {
+ String rs = Integer.toHexString((int) (r * 256));
+ String gs = Integer.toHexString((int) (g * 256));
+ String bs = Integer.toHexString((int) (b * 256));
+ return "#" + rs + gs + bs;
+ }
+
+ /**
+ * Strips a trailing ".git" from the value.
+ *
+ * @param value
+ * @return a stripped value or the original value if .git is not found
+ */
+ public static String stripDotGit(String value) {
+ if (value.toLowerCase().endsWith(".git")) {
+ return value.substring(0, value.length() - 4);
+ }
+ return value;
+ }
+
+ /**
+ * Count the number of lines in a string.
+ *
+ * @param value
+ * @return the line count
+ */
+ public static int countLines(String value) {
+ if (isEmpty(value)) {
+ return 0;
+ }
+ return value.split("\n").length;
+ }
+
+ /**
+ * Returns the file extension of a path.
+ *
+ * @param path
+ * @return a blank string or a file extension
+ */
+ public static String getFileExtension(String path) {
+ int lastDot = path.lastIndexOf('.');
+ if (lastDot > -1) {
+ return path.substring(lastDot + 1);
+ }
+ return "";
+ }
+
+ /**
+ * Replace all occurences of a substring within a string with
+ * another string.
+ *
+ * From Spring StringUtils.
+ *
+ * @param inString String to examine
+ * @param oldPattern String to replace
+ * @param newPattern String to insert
+ * @return a String with the replacements
+ */
+ public static String replace(String inString, String oldPattern, String newPattern) {
+ StringBuilder sb = new StringBuilder();
+ int pos = 0; // our position in the old string
+ int index = inString.indexOf(oldPattern);
+ // the index of an occurrence we've found, or -1
+ int patLen = oldPattern.length();
+ while (index >= 0) {
+ sb.append(inString.substring(pos, index));
+ sb.append(newPattern);
+ pos = index + patLen;
+ index = inString.indexOf(oldPattern, pos);
+ }
+ sb.append(inString.substring(pos));
+ // remember to append any characters to the right of a match
+ return sb.toString();
+ }
+
+ /**
+ * Decodes a string by trying several charsets until one does not throw a
+ * coding exception. Last resort is to interpret as UTF-8 with illegal
+ * character substitution.
+ *
+ * @param content
+ * @param charsets optional
+ * @return a string
+ */
+ public static String decodeString(byte [] content, String... charsets) {
+ Set<String> sets = new LinkedHashSet<String>();
+ if (!ArrayUtils.isEmpty(charsets)) {
+ sets.addAll(Arrays.asList(charsets));
+ }
+ String value = null;
+ sets.addAll(Arrays.asList("UTF-8", "ISO-8859-1", Charset.defaultCharset().name()));
+ for (String charset : sets) {
+ try {
+ Charset cs = Charset.forName(charset);
+ CharsetDecoder decoder = cs.newDecoder();
+ CharBuffer buffer = decoder.decode(ByteBuffer.wrap(content));
+ value = buffer.toString();
+ break;
+ } catch (CharacterCodingException e) {
+ // ignore and advance to the next charset
+ } catch (IllegalCharsetNameException e) {
+ // ignore illegal charset names
+ } catch (UnsupportedCharsetException e) {
+ // ignore unsupported charsets
+ }
+ }
+ if (value.startsWith("\uFEFF")) {
+ // strip UTF-8 BOM
+ return value.substring(1);
+ }
+ return value;
+ }
+
+ /**
+ * Attempt to extract a repository name from a given url using regular
+ * expressions. If no match is made, then return whatever trails after
+ * the final / character.
+ *
+ * @param regexUrls
+ * @return a repository path
+ */
+ public static String extractRepositoryPath(String url, String... urlpatterns) {
+ for (String urlPattern : urlpatterns) {
+ Pattern p = Pattern.compile(urlPattern);
+ Matcher m = p.matcher(url);
+ while (m.find()) {
+ String repositoryPath = m.group(1);
+ return repositoryPath;
+ }
+ }
+ // last resort
+ if (url.lastIndexOf('/') > -1) {
+ return url.substring(url.lastIndexOf('/') + 1);
+ }
+ return url;
+ }
+
+ /**
+ * Converts a string with \nnn sequences into a UTF-8 encoded string.
+ * @param input
+ * @return
+ */
+ public static String convertOctal(String input) {
+ try {
+ ByteArrayOutputStream bytes = new ByteArrayOutputStream();
+ Pattern p = Pattern.compile("(\\\\\\d{3})");
+ Matcher m = p.matcher(input);
+ int i = 0;
+ while (m.find()) {
+ bytes.write(input.substring(i, m.start()).getBytes("UTF-8"));
+ // replace octal encoded value
+ // strip leading \ character
+ String oct = m.group().substring(1);
+ bytes.write(Integer.parseInt(oct, 8));
+ i = m.end();
+ }
+ if (bytes.size() == 0) {
+ // no octal matches
+ return input;
+ } else {
+ if (i < input.length()) {
+ // add remainder of string
+ bytes.write(input.substring(i).getBytes("UTF-8"));
+ }
+ }
+ return bytes.toString("UTF-8");
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+ return input;
+ }
+
+ /**
+ * Returns the first path element of a path string. If no path separator is
+ * found in the path, an empty string is returned.
+ *
+ * @param path
+ * @return the first element in the path
+ */
+ public static String getFirstPathElement(String path) {
+ if (path.indexOf('/') > -1) {
+ return path.substring(0, path.indexOf('/')).trim();
+ }
+ return "";
+ }
+
+ /**
+ * Returns the last path element of a path string
+ *
+ * @param path
+ * @return the last element in the path
+ */
+ public static String getLastPathElement(String path) {
+ if (path.indexOf('/') > -1) {
+ return path.substring(path.lastIndexOf('/') + 1);
+ }
+ return path;
+ }
+
+ /**
+ * Variation of String.matches() which disregards case issues.
+ *
+ * @param regex
+ * @param input
+ * @return true if the pattern matches
+ */
+ public static boolean matchesIgnoreCase(String input, String regex) {
+ Pattern p = Pattern.compile(regex, Pattern.CASE_INSENSITIVE);
+ Matcher m = p.matcher(input);
+ return m.matches();
+ }
+
+ /**
+ * Removes new line and carriage return chars from a string.
+ * If input value is null an empty string is returned.
+ *
+ * @param input
+ * @return a sanitized or empty string
+ */
+ public static String removeNewlines(String input) {
+ if (input == null) {
+ return "";
+ }
+ return input.replace('\n',' ').replace('\r', ' ').trim();
+ }
+} \ No newline at end of file
diff --git a/src/main/java/com/gitblit/utils/SyndicationUtils.java b/src/main/java/com/gitblit/utils/SyndicationUtils.java
new file mode 100644
index 00000000..d01d4691
--- /dev/null
+++ b/src/main/java/com/gitblit/utils/SyndicationUtils.java
@@ -0,0 +1,262 @@
+/*
+ * 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.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.io.OutputStreamWriter;
+import java.net.URLConnection;
+import java.text.MessageFormat;
+import java.util.ArrayList;
+import java.util.List;
+
+import com.gitblit.Constants;
+import com.gitblit.GitBlitException;
+import com.gitblit.models.FeedEntryModel;
+import com.sun.syndication.feed.synd.SyndCategory;
+import com.sun.syndication.feed.synd.SyndCategoryImpl;
+import com.sun.syndication.feed.synd.SyndContent;
+import com.sun.syndication.feed.synd.SyndContentImpl;
+import com.sun.syndication.feed.synd.SyndEntry;
+import com.sun.syndication.feed.synd.SyndEntryImpl;
+import com.sun.syndication.feed.synd.SyndFeed;
+import com.sun.syndication.feed.synd.SyndFeedImpl;
+import com.sun.syndication.feed.synd.SyndImageImpl;
+import com.sun.syndication.io.FeedException;
+import com.sun.syndication.io.SyndFeedInput;
+import com.sun.syndication.io.SyndFeedOutput;
+import com.sun.syndication.io.XmlReader;
+
+/**
+ * Utility class for RSS feeds.
+ *
+ * @author James Moger
+ *
+ */
+public class SyndicationUtils {
+
+ /**
+ * Outputs an RSS feed of the list of entries to the outputstream.
+ *
+ * @param hostUrl
+ * @param feedLink
+ * @param title
+ * @param description
+ * @param entryModels
+ * @param os
+ * @throws IOException
+ * @throws FeedException
+ */
+ public static void toRSS(String hostUrl, String feedLink, String title, String description,
+ List<FeedEntryModel> entryModels, OutputStream os)
+ throws IOException, FeedException {
+
+ SyndFeed feed = new SyndFeedImpl();
+ feed.setFeedType("rss_2.0");
+ feed.setEncoding("UTF-8");
+ feed.setTitle(title);
+ feed.setLink(feedLink);
+ feed.setDescription(description);
+ SyndImageImpl image = new SyndImageImpl();
+ image.setTitle(Constants.NAME);
+ image.setUrl(hostUrl + "/gitblt_25.png");
+ image.setLink(hostUrl);
+ feed.setImage(image);
+
+ List<SyndEntry> entries = new ArrayList<SyndEntry>();
+ for (FeedEntryModel entryModel : entryModels) {
+ SyndEntry entry = new SyndEntryImpl();
+ entry.setTitle(entryModel.title);
+ entry.setAuthor(entryModel.author);
+ entry.setLink(entryModel.link);
+ entry.setPublishedDate(entryModel.published);
+
+ if (entryModel.tags != null && entryModel.tags.size() > 0) {
+ List<SyndCategory> tags = new ArrayList<SyndCategory>();
+ for (String tag : entryModel.tags) {
+ SyndCategoryImpl cat = new SyndCategoryImpl();
+ cat.setName(tag);
+ tags.add(cat);
+ }
+ entry.setCategories(tags);
+ }
+
+ SyndContent content = new SyndContentImpl();
+ if (StringUtils.isEmpty(entryModel.contentType)
+ || entryModel.contentType.equalsIgnoreCase("text/plain")) {
+ content.setType("text/html");
+ content.setValue(StringUtils.breakLinesForHtml(entryModel.content));
+ } else {
+ content.setType(entryModel.contentType);
+ content.setValue(entryModel.content);
+ }
+ entry.setDescription(content);
+
+ entries.add(entry);
+ }
+ feed.setEntries(entries);
+
+ OutputStreamWriter writer = new OutputStreamWriter(os, "UTF-8");
+ SyndFeedOutput output = new SyndFeedOutput();
+ output.output(feed, writer);
+ writer.close();
+ }
+
+ /**
+ * Reads a Gitblit RSS feed.
+ *
+ * @param url
+ * the url of the Gitblit server
+ * @param repository
+ * the repository name
+ * @param branch
+ * the branch name (optional)
+ * @param numberOfEntries
+ * the number of entries to retrieve. if <= 0 the server default
+ * is used.
+ * @param page
+ * 0-indexed. used to paginate the results.
+ * @param username
+ * @param password
+ * @return a list of SyndicationModel entries
+ * @throws {@link IOException}
+ */
+ public static List<FeedEntryModel> readFeed(String url, String repository, String branch,
+ int numberOfEntries, int page, String username, char[] password) throws IOException {
+ // build feed url
+ List<String> parameters = new ArrayList<String>();
+ if (numberOfEntries > 0) {
+ parameters.add("l=" + numberOfEntries);
+ }
+ if (page > 0) {
+ parameters.add("pg=" + page);
+ }
+ if (!StringUtils.isEmpty(branch)) {
+ parameters.add("h=" + branch);
+ }
+ return readFeed(url, parameters, repository, branch, username, password);
+ }
+
+ /**
+ * Reads a Gitblit RSS search feed.
+ *
+ * @param url
+ * the url of the Gitblit server
+ * @param repository
+ * the repository name
+ * @param fragment
+ * the search fragment
+ * @param searchType
+ * the search type (optional, defaults to COMMIT)
+ * @param numberOfEntries
+ * the number of entries to retrieve. if <= 0 the server default
+ * is used.
+ * @param page
+ * 0-indexed. used to paginate the results.
+ * @param username
+ * @param password
+ * @return a list of SyndicationModel entries
+ * @throws {@link IOException}
+ */
+ public static List<FeedEntryModel> readSearchFeed(String url, String repository, String branch,
+ String fragment, Constants.SearchType searchType, int numberOfEntries, int page,
+ String username, char[] password) throws IOException {
+ // determine parameters
+ List<String> parameters = new ArrayList<String>();
+ parameters.add("s=" + StringUtils.encodeURL(fragment));
+ if (numberOfEntries > 0) {
+ parameters.add("l=" + numberOfEntries);
+ }
+ if (page > 0) {
+ parameters.add("pg=" + page);
+ }
+ if (!StringUtils.isEmpty(branch)) {
+ parameters.add("h=" + branch);
+ }
+ if (searchType != null) {
+ parameters.add("st=" + searchType.name());
+ }
+ return readFeed(url, parameters, repository, branch, username, password);
+ }
+
+ /**
+ * Reads a Gitblit RSS feed.
+ *
+ * @param url
+ * the url of the Gitblit server
+ * @param parameters
+ * the list of RSS parameters
+ * @param repository
+ * the repository name
+ * @param username
+ * @param password
+ * @return a list of SyndicationModel entries
+ * @throws {@link IOException}
+ */
+ private static List<FeedEntryModel> readFeed(String url, List<String> parameters,
+ String repository, String branch, String username, char[] password) throws IOException {
+ // build url
+ StringBuilder sb = new StringBuilder();
+ sb.append(MessageFormat.format("{0}" + Constants.SYNDICATION_PATH + "{1}", url, repository));
+ if (parameters.size() > 0) {
+ boolean first = true;
+ for (String parameter : parameters) {
+ if (first) {
+ sb.append('?');
+ first = false;
+ } else {
+ sb.append('&');
+ }
+ sb.append(parameter);
+ }
+ }
+ String feedUrl = sb.toString();
+ URLConnection conn = ConnectionUtils.openReadConnection(feedUrl, username, password);
+ InputStream is = conn.getInputStream();
+ SyndFeedInput input = new SyndFeedInput();
+ SyndFeed feed = null;
+ try {
+ feed = input.build(new XmlReader(is));
+ } catch (FeedException f) {
+ throw new GitBlitException(f);
+ }
+ is.close();
+ List<FeedEntryModel> entries = new ArrayList<FeedEntryModel>();
+ for (Object o : feed.getEntries()) {
+ SyndEntryImpl entry = (SyndEntryImpl) o;
+ FeedEntryModel model = new FeedEntryModel();
+ model.repository = repository;
+ model.branch = branch;
+ model.title = entry.getTitle();
+ model.author = entry.getAuthor();
+ model.published = entry.getPublishedDate();
+ model.link = entry.getLink();
+ model.content = entry.getDescription().getValue();
+ model.contentType = entry.getDescription().getType();
+ if (entry.getCategories() != null && entry.getCategories().size() > 0) {
+ List<String> tags = new ArrayList<String>();
+ for (Object p : entry.getCategories()) {
+ SyndCategory cat = (SyndCategory) p;
+ tags.add(cat.getName());
+ }
+ model.tags = tags;
+ }
+ entries.add(model);
+ }
+ return entries;
+ }
+}
diff --git a/src/main/java/com/gitblit/utils/TicgitUtils.java b/src/main/java/com/gitblit/utils/TicgitUtils.java
new file mode 100644
index 00000000..aab5a3e1
--- /dev/null
+++ b/src/main/java/com/gitblit/utils/TicgitUtils.java
@@ -0,0 +1,148 @@
+/*
+ * 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.text.ParseException;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.revwalk.RevCommit;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.gitblit.models.PathModel;
+import com.gitblit.models.RefModel;
+import com.gitblit.models.TicketModel;
+import com.gitblit.models.TicketModel.Comment;
+
+/**
+ * Utility class for reading Ticgit issues.
+ *
+ * @author James Moger
+ *
+ */
+public class TicgitUtils {
+
+ static final Logger LOGGER = LoggerFactory.getLogger(TicgitUtils.class);
+
+ /**
+ * Returns a RefModel for the Ticgit branch in the repository. If the branch
+ * can not be found, null is returned.
+ *
+ * @param repository
+ * @return a refmodel for the ticgit branch or null
+ */
+ public static RefModel getTicketsBranch(Repository repository) {
+ return JGitUtils.getBranch(repository, "ticgit");
+ }
+
+ /**
+ * Returns a list of all tickets in the ticgit branch of the repository.
+ *
+ * @param repository
+ * @return list of tickets
+ */
+ public static List<TicketModel> getTickets(Repository repository) {
+ RefModel ticgitBranch = getTicketsBranch(repository);
+ if (ticgitBranch == null) {
+ return null;
+ }
+ RevCommit commit = (RevCommit) ticgitBranch.referencedObject;
+ List<PathModel> paths = JGitUtils.getFilesInPath(repository, null, commit);
+ List<TicketModel> tickets = new ArrayList<TicketModel>();
+ for (PathModel ticketFolder : paths) {
+ if (ticketFolder.isTree()) {
+ try {
+ TicketModel t = new TicketModel(ticketFolder.name);
+ loadTicketContents(repository, ticgitBranch, t);
+ tickets.add(t);
+ } catch (Throwable t) {
+ LOGGER.error("Failed to get a ticket!", t);
+ }
+ }
+ }
+ Collections.sort(tickets);
+ Collections.reverse(tickets);
+ return tickets;
+ }
+
+ /**
+ * Returns a TicketModel for the specified ticgit ticket. Returns null if
+ * the ticket does not exist or some other error occurs.
+ *
+ * @param repository
+ * @param ticketFolder
+ * @return a ticket
+ */
+ public static TicketModel getTicket(Repository repository, String ticketFolder) {
+ RefModel ticketsBranch = getTicketsBranch(repository);
+ if (ticketsBranch != null) {
+ try {
+ TicketModel ticket = new TicketModel(ticketFolder);
+ loadTicketContents(repository, ticketsBranch, ticket);
+ return ticket;
+ } catch (Throwable t) {
+ LOGGER.error("Failed to get ticket " + ticketFolder, t);
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Loads the contents of the ticket.
+ *
+ * @param repository
+ * @param ticketsBranch
+ * @param ticket
+ */
+ private static void loadTicketContents(Repository repository, RefModel ticketsBranch,
+ TicketModel ticket) {
+ RevCommit commit = (RevCommit) ticketsBranch.referencedObject;
+ List<PathModel> ticketFiles = JGitUtils.getFilesInPath(repository, ticket.name, commit);
+ for (PathModel file : ticketFiles) {
+ String content = JGitUtils.getStringContent(repository, commit.getTree(), file.path)
+ .trim();
+ if (file.name.equals("TICKET_ID")) {
+ ticket.id = content;
+ } else if (file.name.equals("TITLE")) {
+ ticket.title = content;
+ } else {
+ String[] chunks = file.name.split("_");
+ if (chunks[0].equals("ASSIGNED")) {
+ ticket.handler = content;
+ } else if (chunks[0].equals("COMMENT")) {
+ try {
+ Comment c = new Comment(file.name, content);
+ ticket.comments.add(c);
+ } catch (ParseException e) {
+ LOGGER.error("Failed to parse ticket comment", e);
+ }
+ } else if (chunks[0].equals("TAG")) {
+ if (content.startsWith("TAG_")) {
+ ticket.tags.add(content.substring(4));
+ } else {
+ ticket.tags.add(content);
+ }
+ } else if (chunks[0].equals("STATE")) {
+ ticket.state = content;
+ }
+ }
+ }
+ Collections.sort(ticket.comments);
+ }
+}
diff --git a/src/main/java/com/gitblit/utils/TimeUtils.java b/src/main/java/com/gitblit/utils/TimeUtils.java
new file mode 100644
index 00000000..ec8871c6
--- /dev/null
+++ b/src/main/java/com/gitblit/utils/TimeUtils.java
@@ -0,0 +1,341 @@
+/*
+ * 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.text.MessageFormat;
+import java.text.SimpleDateFormat;
+import java.util.Calendar;
+import java.util.Date;
+import java.util.ResourceBundle;
+
+/**
+ * Utility class of time functions.
+ *
+ * @author James Moger
+ *
+ */
+public class TimeUtils {
+ public static final long MIN = 1000 * 60L;
+
+ public static final long HALFHOUR = MIN * 30L;
+
+ public static final long ONEHOUR = HALFHOUR * 2;
+
+ public static final long ONEDAY = ONEHOUR * 24L;
+
+ public static final long ONEYEAR = ONEDAY * 365L;
+
+ private final ResourceBundle translation;
+
+ public TimeUtils() {
+ this(null);
+ }
+
+ public TimeUtils(ResourceBundle translation) {
+ this.translation = translation;
+ }
+
+ /**
+ * Returns true if date is today.
+ *
+ * @param date
+ * @return true if date is today
+ */
+ public static boolean isToday(Date date) {
+ return (System.currentTimeMillis() - date.getTime()) < ONEDAY;
+ }
+
+ /**
+ * Returns true if date is yesterday.
+ *
+ * @param date
+ * @return true if date is yesterday
+ */
+ public static boolean isYesterday(Date date) {
+ Calendar cal = Calendar.getInstance();
+ cal.setTime(new Date());
+ cal.add(Calendar.DATE, -1);
+ SimpleDateFormat df = new SimpleDateFormat("yyyyMMdd");
+ return df.format(cal.getTime()).equals(df.format(date));
+ }
+
+ /**
+ * Returns the string representation of the duration as days, months and/or
+ * years.
+ *
+ * @param days
+ * @return duration as string in days, months, and/or years
+ */
+ public String duration(int days) {
+ if (days <= 60) {
+ return (days > 1 ? translate(days, "gb.duration.days", "{0} days") : translate("gb.duration.oneDay", "1 day"));
+ } else if (days < 365) {
+ int rem = days % 30;
+ return translate(((days / 30) + (rem >= 15 ? 1 : 0)), "gb.duration.months", "{0} months");
+ } else {
+ int years = days / 365;
+ int rem = days % 365;
+ String yearsString = (years > 1 ? translate(years, "gb.duration.years", "{0} years") : translate("gb.duration.oneYear", "1 year"));
+ if (rem < 30) {
+ if (rem == 0) {
+ return yearsString;
+ } else {
+ return yearsString + (rem >= 15 ? (", " + translate("gb.duration.oneMonth", "1 month")): "");
+ }
+ } else {
+ int months = rem / 30;
+ int remDays = rem % 30;
+ if (remDays >= 15) {
+ months++;
+ }
+ String monthsString = yearsString + ", "
+ + (months > 1 ? translate(months, "gb.duration.months", "{0} months") : translate("gb.duration.oneMonth", "1 month"));
+ return monthsString;
+ }
+ }
+ }
+
+ /**
+ * Returns the number of minutes ago between the start time and the end
+ * time.
+ *
+ * @param date
+ * @param endTime
+ * @param roundup
+ * @return difference in minutes
+ */
+ public static int minutesAgo(Date date, long endTime, boolean roundup) {
+ long diff = endTime - date.getTime();
+ int mins = (int) (diff / MIN);
+ if (roundup && (diff % MIN) >= 30) {
+ mins++;
+ }
+ return mins;
+ }
+
+ /**
+ * Return the difference in minutes between now and the date.
+ *
+ * @param date
+ * @param roundup
+ * @return minutes ago
+ */
+ public static int minutesAgo(Date date, boolean roundup) {
+ return minutesAgo(date, System.currentTimeMillis(), roundup);
+ }
+
+ /**
+ * Return the difference in hours between now and the date.
+ *
+ * @param date
+ * @param roundup
+ * @return hours ago
+ */
+ public static int hoursAgo(Date date, boolean roundup) {
+ long diff = System.currentTimeMillis() - date.getTime();
+ int hours = (int) (diff / ONEHOUR);
+ if (roundup && (diff % ONEHOUR) >= HALFHOUR) {
+ hours++;
+ }
+ return hours;
+ }
+
+ /**
+ * Return the difference in days between now and the date.
+ *
+ * @param date
+ * @return days ago
+ */
+ public static int daysAgo(Date date) {
+ long today = ONEDAY * (System.currentTimeMillis()/ONEDAY);
+ long day = ONEDAY * (date.getTime()/ONEDAY);
+ long diff = today - day;
+ int days = (int) (diff / ONEDAY);
+ return days;
+ }
+
+ public String today() {
+ return translate("gb.time.today", "today");
+ }
+
+ public String yesterday() {
+ return translate("gb.time.yesterday", "yesterday");
+ }
+
+ /**
+ * Returns the string representation of the duration between now and the
+ * date.
+ *
+ * @param date
+ * @return duration as a string
+ */
+ public String timeAgo(Date date) {
+ return timeAgo(date, false);
+ }
+
+ /**
+ * Returns the CSS class for the date based on its age from Now.
+ *
+ * @param date
+ * @return the css class
+ */
+ public String timeAgoCss(Date date) {
+ return timeAgo(date, true);
+ }
+
+ /**
+ * Returns the string representation of the duration OR the css class for
+ * the duration.
+ *
+ * @param date
+ * @param css
+ * @return the string representation of the duration OR the css class
+ */
+ private String timeAgo(Date date, boolean css) {
+ if (isToday(date) || isYesterday(date)) {
+ int mins = minutesAgo(date, true);
+ if (mins >= 120) {
+ if (css) {
+ return "age1";
+ }
+ int hours = hoursAgo(date, true);
+ if (hours > 23) {
+ return yesterday();
+ } else {
+ return translate(hours, "gb.time.hoursAgo", "{0} hours ago");
+ }
+ }
+ if (css) {
+ return "age0";
+ }
+ if (mins > 2) {
+ return translate(mins, "gb.time.minsAgo", "{0} mins ago");
+ }
+ return translate("gb.time.justNow", "just now");
+ } else {
+ int days = daysAgo(date);
+ if (css) {
+ if (days <= 7) {
+ return "age2";
+ } if (days <= 30) {
+ return "age3";
+ } else {
+ return "age4";
+ }
+ }
+ if (days < 365) {
+ if (days <= 30) {
+ return translate(days, "gb.time.daysAgo", "{0} days ago");
+ } else if (days <= 90) {
+ int weeks = days / 7;
+ if (weeks == 12) {
+ return translate(3, "gb.time.monthsAgo", "{0} months ago");
+ } else {
+ return translate(weeks, "gb.time.weeksAgo", "{0} weeks ago");
+ }
+ }
+ int months = days / 30;
+ int weeks = (days % 30) / 7;
+ if (weeks >= 2) {
+ months++;
+ }
+ return translate(months, "gb.time.monthsAgo", "{0} months ago");
+ } else if (days == 365) {
+ return translate("gb.time.oneYearAgo", "1 year ago");
+ } else {
+ int yr = days / 365;
+ days = days % 365;
+ int months = (yr * 12) + (days / 30);
+ if (months > 23) {
+ return translate(yr, "gb.time.yearsAgo", "{0} years ago");
+ } else {
+ return translate(months, "gb.time.monthsAgo", "{0} months ago");
+ }
+ }
+ }
+ }
+
+ public String inFuture(Date date) {
+ long diff = date.getTime() - System.currentTimeMillis();
+ if (diff > ONEDAY) {
+ double days = ((double) diff)/ONEDAY;
+ return translate((int) Math.round(days), "gb.time.inDays", "in {0} days");
+ } else {
+ double hours = ((double) diff)/ONEHOUR;
+ if (hours > 2) {
+ return translate((int) Math.round(hours), "gb.time.inHours", "in {0} hours");
+ } else {
+ int mins = (int) (diff/MIN);
+ return translate(mins, "gb.time.inMinutes", "in {0} minutes");
+ }
+ }
+ }
+
+ private String translate(String key, String defaultValue) {
+ String value = defaultValue;
+ if (translation != null && translation.containsKey(key)) {
+ String aValue = translation.getString(key);
+ if (!StringUtils.isEmpty(aValue)) {
+ value = aValue;
+ }
+ }
+ return value;
+ }
+
+ private String translate(int val, String key, String defaultPattern) {
+ String pattern = defaultPattern;
+ if (translation != null && translation.containsKey(key)) {
+ String aValue = translation.getString(key);
+ if (!StringUtils.isEmpty(aValue)) {
+ pattern = aValue;
+ }
+ }
+ return MessageFormat.format(pattern, val);
+ }
+
+ /**
+ * Convert a frequency string into minutes.
+ *
+ * @param frequency
+ * @return minutes
+ */
+ public static int convertFrequencyToMinutes(String frequency) {
+ // parse the frequency
+ frequency = frequency.toLowerCase();
+ int mins = 60;
+ if (!StringUtils.isEmpty(frequency)) {
+ try {
+ String str = frequency.trim();
+ if (frequency.indexOf(' ') > -1) {
+ str = str.substring(0, str.indexOf(' ')).trim();
+ }
+ mins = (int) Float.parseFloat(str);
+ } catch (NumberFormatException e) {
+ }
+ if (mins < 5) {
+ mins = 5;
+ }
+ }
+ if (frequency.indexOf("day") > -1) {
+ // convert to minutes
+ mins *= 1440;
+ } else if (frequency.indexOf("hour") > -1) {
+ // convert to minutes
+ mins *= 60;
+ }
+ return mins;
+ }
+}
diff --git a/src/main/java/com/gitblit/utils/X509Utils.java b/src/main/java/com/gitblit/utils/X509Utils.java
new file mode 100644
index 00000000..237c8dad
--- /dev/null
+++ b/src/main/java/com/gitblit/utils/X509Utils.java
@@ -0,0 +1,1136 @@
+/*
+ * Copyright 2012 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.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.FileWriter;
+import java.io.IOException;
+import java.io.InputStream;
+import java.lang.reflect.Field;
+import java.math.BigInteger;
+import java.security.InvalidKeyException;
+import java.security.KeyPair;
+import java.security.KeyPairGenerator;
+import java.security.KeyStore;
+import java.security.NoSuchAlgorithmException;
+import java.security.PrivateKey;
+import java.security.SecureRandom;
+import java.security.Security;
+import java.security.SignatureException;
+import java.security.cert.CertPathBuilder;
+import java.security.cert.CertPathBuilderException;
+import java.security.cert.CertStore;
+import java.security.cert.Certificate;
+import java.security.cert.CertificateFactory;
+import java.security.cert.CollectionCertStoreParameters;
+import java.security.cert.PKIXBuilderParameters;
+import java.security.cert.PKIXCertPathBuilderResult;
+import java.security.cert.TrustAnchor;
+import java.security.cert.X509CRL;
+import java.security.cert.X509CertSelector;
+import java.security.cert.X509Certificate;
+import java.text.MessageFormat;
+import java.text.SimpleDateFormat;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Calendar;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.TimeZone;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipOutputStream;
+
+import javax.crypto.Cipher;
+
+import org.bouncycastle.asn1.ASN1ObjectIdentifier;
+import org.bouncycastle.asn1.pkcs.PKCSObjectIdentifiers;
+import org.bouncycastle.asn1.x500.X500Name;
+import org.bouncycastle.asn1.x500.X500NameBuilder;
+import org.bouncycastle.asn1.x500.style.BCStyle;
+import org.bouncycastle.asn1.x509.BasicConstraints;
+import org.bouncycastle.asn1.x509.GeneralName;
+import org.bouncycastle.asn1.x509.GeneralNames;
+import org.bouncycastle.asn1.x509.KeyUsage;
+import org.bouncycastle.asn1.x509.X509Extension;
+import org.bouncycastle.cert.X509CRLHolder;
+import org.bouncycastle.cert.X509v2CRLBuilder;
+import org.bouncycastle.cert.X509v3CertificateBuilder;
+import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter;
+import org.bouncycastle.cert.jcajce.JcaX509ExtensionUtils;
+import org.bouncycastle.cert.jcajce.JcaX509v3CertificateBuilder;
+import org.bouncycastle.jce.PrincipalUtil;
+import org.bouncycastle.jce.interfaces.PKCS12BagAttributeCarrier;
+import org.bouncycastle.openssl.PEMWriter;
+import org.bouncycastle.operator.ContentSigner;
+import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.gitblit.Constants;
+
+/**
+ * Utility class to generate X509 certificates, keystores, and truststores.
+ *
+ * @author James Moger
+ *
+ */
+public class X509Utils {
+
+ public static final String SERVER_KEY_STORE = "serverKeyStore.jks";
+
+ public static final String SERVER_TRUST_STORE = "serverTrustStore.jks";
+
+ public static final String CERTS = "certs";
+
+ public static final String CA_KEY_STORE = "certs/caKeyStore.p12";
+
+ public static final String CA_REVOCATION_LIST = "certs/caRevocationList.crl";
+
+ public static final String CA_CONFIG = "certs/authority.conf";
+
+ public static final String CA_CN = "Gitblit Certificate Authority";
+
+ public static final String CA_ALIAS = CA_CN;
+
+ private static final String BC = org.bouncycastle.jce.provider.BouncyCastleProvider.PROVIDER_NAME;
+
+ private static final int KEY_LENGTH = 2048;
+
+ private static final String KEY_ALGORITHM = "RSA";
+
+ private static final String SIGNING_ALGORITHM = "SHA512withRSA";
+
+ public static final boolean unlimitedStrength;
+
+ private static final Logger logger = LoggerFactory.getLogger(X509Utils.class);
+
+ static {
+ Security.addProvider(new org.bouncycastle.jce.provider.BouncyCastleProvider());
+
+ // check for JCE Unlimited Strength
+ int maxKeyLen = 0;
+ try {
+ maxKeyLen = Cipher.getMaxAllowedKeyLength("AES");
+ } catch (NoSuchAlgorithmException e) {
+ }
+
+ unlimitedStrength = maxKeyLen > 128;
+ if (unlimitedStrength) {
+ logger.info("Using JCE Unlimited Strength Jurisdiction Policy files");
+ } else {
+ logger.info("Using JCE Standard Encryption Policy files, encryption key lengths will be limited");
+ }
+ }
+
+ public static enum RevocationReason {
+ // https://en.wikipedia.org/wiki/Revocation_list
+ unspecified, keyCompromise, caCompromise, affiliationChanged, superseded,
+ cessationOfOperation, certificateHold, unused, removeFromCRL, privilegeWithdrawn,
+ ACompromise;
+
+ public static RevocationReason [] reasons = {
+ unspecified, keyCompromise, caCompromise,
+ affiliationChanged, superseded, cessationOfOperation,
+ privilegeWithdrawn };
+
+ @Override
+ public String toString() {
+ return name() + " (" + ordinal() + ")";
+ }
+ }
+
+ public interface X509Log {
+ void log(String message);
+ }
+
+ public static class X509Metadata {
+
+ // map for distinguished name OIDs
+ public final Map<String, String> oids;
+
+ // CN in distingiushed name
+ public final String commonName;
+
+ // password for store
+ public final String password;
+
+ // password hint for README in bundle
+ public String passwordHint;
+
+ // E or EMAILADDRESS in distinguished name
+ public String emailAddress;
+
+ // start date of generated certificate
+ public Date notBefore;
+
+ // expiraiton date of generated certificate
+ public Date notAfter;
+
+ // hostname of server for which certificate is generated
+ public String serverHostname;
+
+ // displayname of user for README in bundle
+ public String userDisplayname;
+
+ // serialnumber of generated or read certificate
+ public String serialNumber;
+
+ public X509Metadata(String cn, String pwd) {
+ if (StringUtils.isEmpty(cn)) {
+ throw new RuntimeException("Common name required!");
+ }
+ if (StringUtils.isEmpty(pwd)) {
+ throw new RuntimeException("Password required!");
+ }
+
+ commonName = cn;
+ password = pwd;
+ Calendar c = Calendar.getInstance(TimeZone.getDefault());
+ c.set(Calendar.SECOND, 0);
+ c.set(Calendar.MILLISECOND, 0);
+ notBefore = c.getTime();
+ c.add(Calendar.YEAR, 1);
+ c.add(Calendar.DATE, 1);
+ notAfter = c.getTime();
+ oids = new HashMap<String, String>();
+ }
+
+ public X509Metadata clone(String commonName, String password) {
+ X509Metadata clone = new X509Metadata(commonName, password);
+ clone.emailAddress = emailAddress;
+ clone.notBefore = notBefore;
+ clone.notAfter = notAfter;
+ clone.oids.putAll(oids);
+ clone.passwordHint = passwordHint;
+ clone.serverHostname = serverHostname;
+ clone.userDisplayname = userDisplayname;
+ return clone;
+ }
+
+ public String getOID(String oid, String defaultValue) {
+ if (oids.containsKey(oid)) {
+ return oids.get(oid);
+ }
+ return defaultValue;
+ }
+
+ public void setOID(String oid, String value) {
+ if (StringUtils.isEmpty(value)) {
+ oids.remove(oid);
+ } else {
+ oids.put(oid, value);
+ }
+ }
+ }
+
+ /**
+ * Prepare all the certificates and stores necessary for a Gitblit GO server.
+ *
+ * @param metadata
+ * @param folder
+ * @param x509log
+ */
+ public static void prepareX509Infrastructure(X509Metadata metadata, File folder, X509Log x509log) {
+ // make the specified folder, if necessary
+ folder.mkdirs();
+
+ // Gitblit CA certificate
+ File caKeyStore = new File(folder, CA_KEY_STORE);
+ if (!caKeyStore.exists()) {
+ logger.info(MessageFormat.format("Generating {0} ({1})", CA_CN, caKeyStore.getAbsolutePath()));
+ X509Certificate caCert = newCertificateAuthority(metadata, caKeyStore, x509log);
+ saveCertificate(caCert, new File(caKeyStore.getParentFile(), "ca.cer"));
+ }
+
+ // Gitblit CRL
+ File caRevocationList = new File(folder, CA_REVOCATION_LIST);
+ if (!caRevocationList.exists()) {
+ logger.info(MessageFormat.format("Generating {0} CRL ({1})", CA_CN, caRevocationList.getAbsolutePath()));
+ newCertificateRevocationList(caRevocationList, caKeyStore, metadata.password);
+ x509log.log("new certificate revocation list created");
+ }
+
+ // rename the old keystore to the new name
+ File oldKeyStore = new File(folder, "keystore");
+ if (oldKeyStore.exists()) {
+ oldKeyStore.renameTo(new File(folder, SERVER_KEY_STORE));
+ logger.info(MessageFormat.format("Renaming {0} to {1}", oldKeyStore.getName(), SERVER_KEY_STORE));
+ }
+
+ // create web SSL certificate signed by CA
+ File serverKeyStore = new File(folder, SERVER_KEY_STORE);
+ if (!serverKeyStore.exists()) {
+ logger.info(MessageFormat.format("Generating SSL certificate for {0} signed by {1} ({2})", metadata.commonName, CA_CN, serverKeyStore.getAbsolutePath()));
+ PrivateKey caPrivateKey = getPrivateKey(CA_ALIAS, caKeyStore, metadata.password);
+ X509Certificate caCert = getCertificate(CA_ALIAS, caKeyStore, metadata.password);
+ newSSLCertificate(metadata, caPrivateKey, caCert, serverKeyStore, x509log);
+ }
+
+ // server certificate trust store holds trusted public certificates
+ File serverTrustStore = new File(folder, X509Utils.SERVER_TRUST_STORE);
+ if (!serverTrustStore.exists()) {
+ logger.info(MessageFormat.format("Importing {0} into trust store ({1})", CA_ALIAS, serverTrustStore.getAbsolutePath()));
+ X509Certificate caCert = getCertificate(CA_ALIAS, caKeyStore, metadata.password);
+ addTrustedCertificate(CA_ALIAS, caCert, serverTrustStore, metadata.password);
+ }
+ }
+
+ /**
+ * Open a keystore. Store type is determined by file extension of name. If
+ * undetermined, JKS is assumed. The keystore does not need to exist.
+ *
+ * @param storeFile
+ * @param storePassword
+ * @return a KeyStore
+ */
+ public static KeyStore openKeyStore(File storeFile, String storePassword) {
+ String lc = storeFile.getName().toLowerCase();
+ String type = "JKS";
+ String provider = null;
+ if (lc.endsWith(".p12") || lc.endsWith(".pfx")) {
+ type = "PKCS12";
+ provider = BC;
+ }
+
+ try {
+ KeyStore store;
+ if (provider == null) {
+ store = KeyStore.getInstance(type);
+ } else {
+ store = KeyStore.getInstance(type, provider);
+ }
+ if (storeFile.exists()) {
+ FileInputStream fis = null;
+ try {
+ fis = new FileInputStream(storeFile);
+ store.load(fis, storePassword.toCharArray());
+ } finally {
+ if (fis != null) {
+ fis.close();
+ }
+ }
+ } else {
+ store.load(null);
+ }
+ return store;
+ } catch (Exception e) {
+ throw new RuntimeException("Could not open keystore " + storeFile, e);
+ }
+ }
+
+ /**
+ * Saves the keystore to the specified file.
+ *
+ * @param targetStoreFile
+ * @param store
+ * @param password
+ */
+ public static void saveKeyStore(File targetStoreFile, KeyStore store, String password) {
+ File folder = targetStoreFile.getAbsoluteFile().getParentFile();
+ if (!folder.exists()) {
+ folder.mkdirs();
+ }
+ File tmpFile = new File(folder, Long.toHexString(System.currentTimeMillis()) + ".tmp");
+ FileOutputStream fos = null;
+ try {
+ fos = new FileOutputStream(tmpFile);
+ store.store(fos, password.toCharArray());
+ fos.flush();
+ fos.close();
+ if (targetStoreFile.exists()) {
+ targetStoreFile.delete();
+ }
+ tmpFile.renameTo(targetStoreFile);
+ } catch (IOException e) {
+ String message = e.getMessage().toLowerCase();
+ if (message.contains("illegal key size")) {
+ throw new RuntimeException("Illegal Key Size! You might consider installing the JCE Unlimited Strength Jurisdiction Policy files for your JVM.");
+ } else {
+ throw new RuntimeException("Could not save keystore " + targetStoreFile, e);
+ }
+ } catch (Exception e) {
+ throw new RuntimeException("Could not save keystore " + targetStoreFile, e);
+ } finally {
+ if (fos != null) {
+ try {
+ fos.close();
+ } catch (IOException e) {
+ }
+ }
+
+ if (tmpFile.exists()) {
+ tmpFile.delete();
+ }
+ }
+ }
+
+ /**
+ * Retrieves the X509 certificate with the specified alias from the certificate
+ * store.
+ *
+ * @param alias
+ * @param storeFile
+ * @param storePassword
+ * @return the certificate
+ */
+ public static X509Certificate getCertificate(String alias, File storeFile, String storePassword) {
+ try {
+ KeyStore store = openKeyStore(storeFile, storePassword);
+ X509Certificate caCert = (X509Certificate) store.getCertificate(alias);
+ return caCert;
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ /**
+ * Retrieves the private key for the specified alias from the certificate
+ * store.
+ *
+ * @param alias
+ * @param storeFile
+ * @param storePassword
+ * @return the private key
+ */
+ public static PrivateKey getPrivateKey(String alias, File storeFile, String storePassword) {
+ try {
+ KeyStore store = openKeyStore(storeFile, storePassword);
+ PrivateKey key = (PrivateKey) store.getKey(alias, storePassword.toCharArray());
+ return key;
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ /**
+ * Saves the certificate to the file system. If the destination filename
+ * ends with the pem extension, the certificate is written in the PEM format,
+ * otherwise the certificate is written in the DER format.
+ *
+ * @param cert
+ * @param targetFile
+ */
+ public static void saveCertificate(X509Certificate cert, File targetFile) {
+ File folder = targetFile.getAbsoluteFile().getParentFile();
+ if (!folder.exists()) {
+ folder.mkdirs();
+ }
+ File tmpFile = new File(folder, Long.toHexString(System.currentTimeMillis()) + ".tmp");
+ try {
+ boolean asPem = targetFile.getName().toLowerCase().endsWith(".pem");
+ if (asPem) {
+ // PEM encoded X509
+ PEMWriter pemWriter = null;
+ try {
+ pemWriter = new PEMWriter(new FileWriter(tmpFile));
+ pemWriter.writeObject(cert);
+ pemWriter.flush();
+ } finally {
+ if (pemWriter != null) {
+ pemWriter.close();
+ }
+ }
+ } else {
+ // DER encoded X509
+ FileOutputStream fos = null;
+ try {
+ fos = new FileOutputStream(tmpFile);
+ fos.write(cert.getEncoded());
+ fos.flush();
+ } finally {
+ if (fos != null) {
+ fos.close();
+ }
+ }
+ }
+
+ // rename tmp file to target
+ if (targetFile.exists()) {
+ targetFile.delete();
+ }
+ tmpFile.renameTo(targetFile);
+ } catch (Exception e) {
+ if (tmpFile.exists()) {
+ tmpFile.delete();
+ }
+ throw new RuntimeException("Failed to save certificate " + cert.getSubjectX500Principal().getName(), e);
+ }
+ }
+
+ /**
+ * Generate a new keypair.
+ *
+ * @return a keypair
+ * @throws Exception
+ */
+ private static KeyPair newKeyPair() throws Exception {
+ KeyPairGenerator kpGen = KeyPairGenerator.getInstance(KEY_ALGORITHM, BC);
+ kpGen.initialize(KEY_LENGTH, new SecureRandom());
+ return kpGen.generateKeyPair();
+ }
+
+ /**
+ * Builds a distinguished name from the X509Metadata.
+ *
+ * @return a DN
+ */
+ private static X500Name buildDistinguishedName(X509Metadata metadata) {
+ X500NameBuilder dnBuilder = new X500NameBuilder(BCStyle.INSTANCE);
+ setOID(dnBuilder, metadata, "C", null);
+ setOID(dnBuilder, metadata, "ST", null);
+ setOID(dnBuilder, metadata, "L", null);
+ setOID(dnBuilder, metadata, "O", Constants.NAME);
+ setOID(dnBuilder, metadata, "OU", Constants.NAME);
+ setOID(dnBuilder, metadata, "E", metadata.emailAddress);
+ setOID(dnBuilder, metadata, "CN", metadata.commonName);
+ X500Name dn = dnBuilder.build();
+ return dn;
+ }
+
+ private static void setOID(X500NameBuilder dnBuilder, X509Metadata metadata,
+ String oid, String defaultValue) {
+
+ String value = null;
+ if (metadata.oids != null && metadata.oids.containsKey(oid)) {
+ value = metadata.oids.get(oid);
+ }
+ if (StringUtils.isEmpty(value)) {
+ value = defaultValue;
+ }
+
+ if (!StringUtils.isEmpty(value)) {
+ try {
+ Field field = BCStyle.class.getField(oid);
+ ASN1ObjectIdentifier objectId = (ASN1ObjectIdentifier) field.get(null);
+ dnBuilder.addRDN(objectId, value);
+ } catch (Exception e) {
+ logger.error(MessageFormat.format("Failed to set OID \"{0}\"!", oid) ,e);
+ }
+ }
+ }
+
+ /**
+ * Creates a new SSL certificate signed by the CA private key and stored in
+ * keyStore.
+ *
+ * @param sslMetadata
+ * @param caPrivateKey
+ * @param caCert
+ * @param targetStoreFile
+ * @param x509log
+ */
+ public static X509Certificate newSSLCertificate(X509Metadata sslMetadata, PrivateKey caPrivateKey, X509Certificate caCert, File targetStoreFile, X509Log x509log) {
+ try {
+ KeyPair pair = newKeyPair();
+
+ X500Name webDN = buildDistinguishedName(sslMetadata);
+ X500Name issuerDN = new X500Name(PrincipalUtil.getIssuerX509Principal(caCert).getName());
+
+ X509v3CertificateBuilder certBuilder = new JcaX509v3CertificateBuilder(
+ issuerDN,
+ BigInteger.valueOf(System.currentTimeMillis()),
+ sslMetadata.notBefore,
+ sslMetadata.notAfter,
+ webDN,
+ pair.getPublic());
+
+ JcaX509ExtensionUtils extUtils = new JcaX509ExtensionUtils();
+ certBuilder.addExtension(X509Extension.subjectKeyIdentifier, false, extUtils.createSubjectKeyIdentifier(pair.getPublic()));
+ certBuilder.addExtension(X509Extension.basicConstraints, false, new BasicConstraints(false));
+ certBuilder.addExtension(X509Extension.authorityKeyIdentifier, false, extUtils.createAuthorityKeyIdentifier(caCert.getPublicKey()));
+
+ // support alternateSubjectNames for SSL certificates
+ List<GeneralName> altNames = new ArrayList<GeneralName>();
+ if (HttpUtils.isIpAddress(sslMetadata.commonName)) {
+ altNames.add(new GeneralName(GeneralName.iPAddress, sslMetadata.commonName));
+ }
+ if (altNames.size() > 0) {
+ GeneralNames subjectAltName = new GeneralNames(altNames.toArray(new GeneralName [altNames.size()]));
+ certBuilder.addExtension(X509Extension.subjectAlternativeName, false, subjectAltName);
+ }
+
+ ContentSigner caSigner = new JcaContentSignerBuilder(SIGNING_ALGORITHM)
+ .setProvider(BC).build(caPrivateKey);
+ X509Certificate cert = new JcaX509CertificateConverter().setProvider(BC)
+ .getCertificate(certBuilder.build(caSigner));
+
+ cert.checkValidity(new Date());
+ cert.verify(caCert.getPublicKey());
+
+ // Save to keystore
+ KeyStore serverStore = openKeyStore(targetStoreFile, sslMetadata.password);
+ serverStore.setKeyEntry(sslMetadata.commonName, pair.getPrivate(), sslMetadata.password.toCharArray(),
+ new Certificate[] { cert, caCert });
+ saveKeyStore(targetStoreFile, serverStore, sslMetadata.password);
+
+ x509log.log(MessageFormat.format("New SSL certificate {0,number,0} [{1}]", cert.getSerialNumber(), cert.getSubjectDN().getName()));
+
+ // update serial number in metadata object
+ sslMetadata.serialNumber = cert.getSerialNumber().toString();
+
+ return cert;
+ } catch (Throwable t) {
+ throw new RuntimeException("Failed to generate SSL certificate!", t);
+ }
+ }
+
+ /**
+ * Creates a new certificate authority PKCS#12 store. This function will
+ * destroy any existing CA store.
+ *
+ * @param metadata
+ * @param storeFile
+ * @param keystorePassword
+ * @param x509log
+ * @return
+ */
+ public static X509Certificate newCertificateAuthority(X509Metadata metadata, File storeFile, X509Log x509log) {
+ try {
+ KeyPair caPair = newKeyPair();
+
+ ContentSigner caSigner = new JcaContentSignerBuilder(SIGNING_ALGORITHM).setProvider(BC).build(caPair.getPrivate());
+
+ // clone metadata
+ X509Metadata caMetadata = metadata.clone(CA_CN, metadata.password);
+ X500Name issuerDN = buildDistinguishedName(caMetadata);
+
+ // Generate self-signed certificate
+ X509v3CertificateBuilder caBuilder = new JcaX509v3CertificateBuilder(
+ issuerDN,
+ BigInteger.valueOf(System.currentTimeMillis()),
+ caMetadata.notBefore,
+ caMetadata.notAfter,
+ issuerDN,
+ caPair.getPublic());
+
+ JcaX509ExtensionUtils extUtils = new JcaX509ExtensionUtils();
+ caBuilder.addExtension(X509Extension.subjectKeyIdentifier, false, extUtils.createSubjectKeyIdentifier(caPair.getPublic()));
+ caBuilder.addExtension(X509Extension.authorityKeyIdentifier, false, extUtils.createAuthorityKeyIdentifier(caPair.getPublic()));
+ caBuilder.addExtension(X509Extension.basicConstraints, false, new BasicConstraints(true));
+ caBuilder.addExtension(X509Extension.keyUsage, true, new KeyUsage(KeyUsage.digitalSignature | KeyUsage.keyCertSign | KeyUsage.cRLSign));
+
+ JcaX509CertificateConverter converter = new JcaX509CertificateConverter().setProvider(BC);
+ X509Certificate cert = converter.getCertificate(caBuilder.build(caSigner));
+
+ // confirm the validity of the CA certificate
+ cert.checkValidity(new Date());
+ cert.verify(cert.getPublicKey());
+
+ // Delete existing keystore
+ if (storeFile.exists()) {
+ storeFile.delete();
+ }
+
+ // Save private key and certificate to new keystore
+ KeyStore store = openKeyStore(storeFile, caMetadata.password);
+ store.setKeyEntry(CA_ALIAS, caPair.getPrivate(), caMetadata.password.toCharArray(),
+ new Certificate[] { cert });
+ saveKeyStore(storeFile, store, caMetadata.password);
+
+ x509log.log(MessageFormat.format("New CA certificate {0,number,0} [{1}]", cert.getSerialNumber(), cert.getIssuerDN().getName()));
+
+ // update serial number in metadata object
+ caMetadata.serialNumber = cert.getSerialNumber().toString();
+
+ return cert;
+ } catch (Throwable t) {
+ throw new RuntimeException("Failed to generate Gitblit CA certificate!", t);
+ }
+ }
+
+ /**
+ * Creates a new certificate revocation list (CRL). This function will
+ * destroy any existing CRL file.
+ *
+ * @param caRevocationList
+ * @param storeFile
+ * @param keystorePassword
+ * @return
+ */
+ public static void newCertificateRevocationList(File caRevocationList, File caKeystoreFile, String caKeystorePassword) {
+ try {
+ // read the Gitblit CA key and certificate
+ KeyStore store = openKeyStore(caKeystoreFile, caKeystorePassword);
+ PrivateKey caPrivateKey = (PrivateKey) store.getKey(CA_ALIAS, caKeystorePassword.toCharArray());
+ X509Certificate caCert = (X509Certificate) store.getCertificate(CA_ALIAS);
+
+ X500Name issuerDN = new X500Name(PrincipalUtil.getIssuerX509Principal(caCert).getName());
+ X509v2CRLBuilder crlBuilder = new X509v2CRLBuilder(issuerDN, new Date());
+
+ // build and sign CRL with CA private key
+ ContentSigner signer = new JcaContentSignerBuilder(SIGNING_ALGORITHM).setProvider(BC).build(caPrivateKey);
+ X509CRLHolder crl = crlBuilder.build(signer);
+
+ File tmpFile = new File(caRevocationList.getParentFile(), Long.toHexString(System.currentTimeMillis()) + ".tmp");
+ FileOutputStream fos = null;
+ try {
+ fos = new FileOutputStream(tmpFile);
+ fos.write(crl.getEncoded());
+ fos.flush();
+ fos.close();
+ if (caRevocationList.exists()) {
+ caRevocationList.delete();
+ }
+ tmpFile.renameTo(caRevocationList);
+ } finally {
+ if (fos != null) {
+ fos.close();
+ }
+ if (tmpFile.exists()) {
+ tmpFile.delete();
+ }
+ }
+ } catch (Exception e) {
+ throw new RuntimeException("Failed to create new certificate revocation list " + caRevocationList, e);
+ }
+ }
+
+ /**
+ * Imports a certificate into the trust store.
+ *
+ * @param alias
+ * @param cert
+ * @param storeFile
+ * @param storePassword
+ */
+ public static void addTrustedCertificate(String alias, X509Certificate cert, File storeFile, String storePassword) {
+ try {
+ KeyStore store = openKeyStore(storeFile, storePassword);
+ store.setCertificateEntry(alias, cert);
+ saveKeyStore(storeFile, store, storePassword);
+ } catch (Exception e) {
+ throw new RuntimeException("Failed to import certificate into trust store " + storeFile, e);
+ }
+ }
+
+ /**
+ * Creates a new client certificate PKCS#12 and PEM store. Any existing
+ * stores are destroyed. After generation, the certificates are bundled
+ * into a zip file with a personalized README file.
+ *
+ * The zip file reference is returned.
+ *
+ * @param clientMetadata a container for dynamic parameters needed for generation
+ * @param caKeystoreFile
+ * @param caKeystorePassword
+ * @param x509log
+ * @return a zip file containing the P12, PEM, and personalized README
+ */
+ public static File newClientBundle(X509Metadata clientMetadata, File caKeystoreFile,
+ String caKeystorePassword, X509Log x509log) {
+ try {
+ // read the Gitblit CA key and certificate
+ KeyStore store = openKeyStore(caKeystoreFile, caKeystorePassword);
+ PrivateKey caPrivateKey = (PrivateKey) store.getKey(CA_ALIAS, caKeystorePassword.toCharArray());
+ X509Certificate caCert = (X509Certificate) store.getCertificate(CA_ALIAS);
+
+ // generate the P12 and PEM files
+ File targetFolder = new File(caKeystoreFile.getParentFile(), clientMetadata.commonName);
+ X509Certificate cert = newClientCertificate(clientMetadata, caPrivateKey, caCert, targetFolder);
+ x509log.log(MessageFormat.format("New client certificate {0,number,0} [{1}]", cert.getSerialNumber(), cert.getSubjectDN().getName()));
+
+ // process template message
+ String readme = processTemplate(new File(caKeystoreFile.getParentFile(), "instructions.tmpl"), clientMetadata);
+
+ // Create a zip bundle with the p12, pem, and a personalized readme
+ File zipFile = new File(targetFolder, clientMetadata.commonName + ".zip");
+ if (zipFile.exists()) {
+ zipFile.delete();
+ }
+ ZipOutputStream zos = null;
+ try {
+ zos = new ZipOutputStream(new FileOutputStream(zipFile));
+ File p12File = new File(targetFolder, clientMetadata.commonName + ".p12");
+ if (p12File.exists()) {
+ zos.putNextEntry(new ZipEntry(p12File.getName()));
+ zos.write(FileUtils.readContent(p12File));
+ zos.closeEntry();
+ }
+ File pemFile = new File(targetFolder, clientMetadata.commonName + ".pem");
+ if (pemFile.exists()) {
+ zos.putNextEntry(new ZipEntry(pemFile.getName()));
+ zos.write(FileUtils.readContent(pemFile));
+ zos.closeEntry();
+ }
+
+ // include user's public certificate
+ zos.putNextEntry(new ZipEntry(clientMetadata.commonName + ".cer"));
+ zos.write(cert.getEncoded());
+ zos.closeEntry();
+
+ // include CA public certificate
+ zos.putNextEntry(new ZipEntry("ca.cer"));
+ zos.write(caCert.getEncoded());
+ zos.closeEntry();
+
+ if (readme != null) {
+ zos.putNextEntry(new ZipEntry("README.TXT"));
+ zos.write(readme.getBytes("UTF-8"));
+ zos.closeEntry();
+ }
+ zos.flush();
+ } finally {
+ if (zos != null) {
+ zos.close();
+ }
+ }
+
+ return zipFile;
+ } catch (Throwable t) {
+ throw new RuntimeException("Failed to generate client bundle!", t);
+ }
+ }
+
+ /**
+ * Creates a new client certificate PKCS#12 and PEM store. Any existing
+ * stores are destroyed.
+ *
+ * @param clientMetadata a container for dynamic parameters needed for generation
+ * @param caKeystoreFile
+ * @param caKeystorePassword
+ * @param targetFolder
+ * @return
+ */
+ public static X509Certificate newClientCertificate(X509Metadata clientMetadata,
+ PrivateKey caPrivateKey, X509Certificate caCert, File targetFolder) {
+ try {
+ KeyPair pair = newKeyPair();
+
+ X500Name userDN = buildDistinguishedName(clientMetadata);
+ X500Name issuerDN = new X500Name(PrincipalUtil.getIssuerX509Principal(caCert).getName());
+
+ // create a new certificate signed by the Gitblit CA certificate
+ X509v3CertificateBuilder certBuilder = new JcaX509v3CertificateBuilder(
+ issuerDN,
+ BigInteger.valueOf(System.currentTimeMillis()),
+ clientMetadata.notBefore,
+ clientMetadata.notAfter,
+ userDN,
+ pair.getPublic());
+
+ JcaX509ExtensionUtils extUtils = new JcaX509ExtensionUtils();
+ certBuilder.addExtension(X509Extension.subjectKeyIdentifier, false, extUtils.createSubjectKeyIdentifier(pair.getPublic()));
+ certBuilder.addExtension(X509Extension.basicConstraints, false, new BasicConstraints(false));
+ certBuilder.addExtension(X509Extension.authorityKeyIdentifier, false, extUtils.createAuthorityKeyIdentifier(caCert.getPublicKey()));
+ certBuilder.addExtension(X509Extension.keyUsage, true, new KeyUsage(KeyUsage.keyEncipherment | KeyUsage.digitalSignature));
+ if (!StringUtils.isEmpty(clientMetadata.emailAddress)) {
+ GeneralNames subjectAltName = new GeneralNames(
+ new GeneralName(GeneralName.rfc822Name, clientMetadata.emailAddress));
+ certBuilder.addExtension(X509Extension.subjectAlternativeName, false, subjectAltName);
+ }
+
+ ContentSigner signer = new JcaContentSignerBuilder(SIGNING_ALGORITHM).setProvider(BC).build(caPrivateKey);
+
+ X509Certificate userCert = new JcaX509CertificateConverter().setProvider(BC).getCertificate(certBuilder.build(signer));
+ PKCS12BagAttributeCarrier bagAttr = (PKCS12BagAttributeCarrier)pair.getPrivate();
+ bagAttr.setBagAttribute(PKCSObjectIdentifiers.pkcs_9_at_localKeyId,
+ extUtils.createSubjectKeyIdentifier(pair.getPublic()));
+
+ // confirm the validity of the user certificate
+ userCert.checkValidity();
+ userCert.verify(caCert.getPublicKey());
+ userCert.getIssuerDN().equals(caCert.getSubjectDN());
+
+ // verify user certificate chain
+ verifyChain(userCert, caCert);
+
+ targetFolder.mkdirs();
+
+ // save certificate, stamped with unique name
+ String date = new SimpleDateFormat("yyyyMMdd").format(new Date());
+ String id = date;
+ File certFile = new File(targetFolder, id + ".cer");
+ int count = 0;
+ while (certFile.exists()) {
+ id = date + "_" + Character.toString((char) (0x61 + count));
+ certFile = new File(targetFolder, id + ".cer");
+ count++;
+ }
+
+ // save user private key, user certificate and CA certificate to a PKCS#12 store
+ File p12File = new File(targetFolder, clientMetadata.commonName + ".p12");
+ if (p12File.exists()) {
+ p12File.delete();
+ }
+ KeyStore userStore = openKeyStore(p12File, clientMetadata.password);
+ userStore.setKeyEntry(MessageFormat.format("Gitblit ({0}) {1} {2}", clientMetadata.serverHostname, clientMetadata.userDisplayname, id), pair.getPrivate(), null, new Certificate [] { userCert });
+ userStore.setCertificateEntry(MessageFormat.format("Gitblit ({0}) Certificate Authority", clientMetadata.serverHostname), caCert);
+ saveKeyStore(p12File, userStore, clientMetadata.password);
+
+ // save user private key, user certificate, and CA certificate to a PEM store
+ File pemFile = new File(targetFolder, clientMetadata.commonName + ".pem");
+ if (pemFile.exists()) {
+ pemFile.delete();
+ }
+ PEMWriter pemWriter = new PEMWriter(new FileWriter(pemFile));
+ pemWriter.writeObject(pair.getPrivate(), "DES-EDE3-CBC", clientMetadata.password.toCharArray(), new SecureRandom());
+ pemWriter.writeObject(userCert);
+ pemWriter.writeObject(caCert);
+ pemWriter.flush();
+ pemWriter.close();
+
+ // save certificate after successfully creating the key stores
+ saveCertificate(userCert, certFile);
+
+ // update serial number in metadata object
+ clientMetadata.serialNumber = userCert.getSerialNumber().toString();
+
+ return userCert;
+ } catch (Throwable t) {
+ throw new RuntimeException("Failed to generate client certificate!", t);
+ }
+ }
+
+ /**
+ * Verifies a certificate's chain to ensure that it will function properly.
+ *
+ * @param testCert
+ * @param additionalCerts
+ * @return
+ */
+ public static PKIXCertPathBuilderResult verifyChain(X509Certificate testCert, X509Certificate... additionalCerts) {
+ try {
+ // Check for self-signed certificate
+ if (isSelfSigned(testCert)) {
+ throw new RuntimeException("The certificate is self-signed. Nothing to verify.");
+ }
+
+ // Prepare a set of all certificates
+ // chain builder must have all certs, including cert to validate
+ // http://stackoverflow.com/a/10788392
+ Set<X509Certificate> certs = new HashSet<X509Certificate>();
+ certs.add(testCert);
+ certs.addAll(Arrays.asList(additionalCerts));
+
+ // Attempt to build the certification chain and verify it
+ // Create the selector that specifies the starting certificate
+ X509CertSelector selector = new X509CertSelector();
+ selector.setCertificate(testCert);
+
+ // Create the trust anchors (set of root CA certificates)
+ Set<TrustAnchor> trustAnchors = new HashSet<TrustAnchor>();
+ for (X509Certificate cert : additionalCerts) {
+ if (isSelfSigned(cert)) {
+ trustAnchors.add(new TrustAnchor(cert, null));
+ }
+ }
+
+ // Configure the PKIX certificate builder
+ PKIXBuilderParameters pkixParams = new PKIXBuilderParameters(trustAnchors, selector);
+ pkixParams.setRevocationEnabled(false);
+ pkixParams.addCertStore(CertStore.getInstance("Collection", new CollectionCertStoreParameters(certs), BC));
+
+ // Build and verify the certification chain
+ CertPathBuilder builder = CertPathBuilder.getInstance("PKIX", BC);
+ PKIXCertPathBuilderResult verifiedCertChain = (PKIXCertPathBuilderResult) builder.build(pkixParams);
+
+ // The chain is built and verified
+ return verifiedCertChain;
+ } catch (CertPathBuilderException e) {
+ throw new RuntimeException("Error building certification path: " + testCert.getSubjectX500Principal(), e);
+ } catch (Exception e) {
+ throw new RuntimeException("Error verifying the certificate: " + testCert.getSubjectX500Principal(), e);
+ }
+ }
+
+ /**
+ * Checks whether given X.509 certificate is self-signed.
+ *
+ * @param cert
+ * @return true if the certificate is self-signed
+ */
+ public static boolean isSelfSigned(X509Certificate cert) {
+ try {
+ cert.verify(cert.getPublicKey());
+ return true;
+ } catch (SignatureException e) {
+ return false;
+ } catch (InvalidKeyException e) {
+ return false;
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ public static String processTemplate(File template, X509Metadata metadata) {
+ String content = null;
+ if (template.exists()) {
+ String message = FileUtils.readContent(template, "\n");
+ if (!StringUtils.isEmpty(message)) {
+ content = message;
+ if (!StringUtils.isEmpty(metadata.serverHostname)) {
+ content = content.replace("$serverHostname", metadata.serverHostname);
+ }
+ if (!StringUtils.isEmpty(metadata.commonName)) {
+ content = content.replace("$username", metadata.commonName);
+ }
+ if (!StringUtils.isEmpty(metadata.userDisplayname)) {
+ content = content.replace("$userDisplayname", metadata.userDisplayname);
+ }
+ if (!StringUtils.isEmpty(metadata.passwordHint)) {
+ content = content.replace("$storePasswordHint", metadata.passwordHint);
+ }
+ }
+ }
+ return content;
+ }
+
+ /**
+ * Revoke a certificate.
+ *
+ * @param cert
+ * @param reason
+ * @param caRevocationList
+ * @param caKeystoreFile
+ * @param caKeystorePassword
+ * @param x509log
+ * @return true if the certificate has been revoked
+ */
+ public static boolean revoke(X509Certificate cert, RevocationReason reason,
+ File caRevocationList, File caKeystoreFile, String caKeystorePassword,
+ X509Log x509log) {
+ try {
+ // read the Gitblit CA key and certificate
+ KeyStore store = openKeyStore(caKeystoreFile, caKeystorePassword);
+ PrivateKey caPrivateKey = (PrivateKey) store.getKey(CA_ALIAS, caKeystorePassword.toCharArray());
+ return revoke(cert, reason, caRevocationList, caPrivateKey, x509log);
+ } catch (Exception e) {
+ logger.error(MessageFormat.format("Failed to revoke certificate {0,number,0} [{1}] in {2}",
+ cert.getSerialNumber(), cert.getSubjectDN().getName(), caRevocationList));
+ }
+ return false;
+ }
+
+ /**
+ * Revoke a certificate.
+ *
+ * @param cert
+ * @param reason
+ * @param caRevocationList
+ * @param caPrivateKey
+ * @param x509log
+ * @return true if the certificate has been revoked
+ */
+ public static boolean revoke(X509Certificate cert, RevocationReason reason,
+ File caRevocationList, PrivateKey caPrivateKey, X509Log x509log) {
+ try {
+ X500Name issuerDN = new X500Name(PrincipalUtil.getIssuerX509Principal(cert).getName());
+ X509v2CRLBuilder crlBuilder = new X509v2CRLBuilder(issuerDN, new Date());
+ if (caRevocationList.exists()) {
+ byte [] data = FileUtils.readContent(caRevocationList);
+ X509CRLHolder crl = new X509CRLHolder(data);
+ crlBuilder.addCRL(crl);
+ }
+ crlBuilder.addCRLEntry(cert.getSerialNumber(), new Date(), reason.ordinal());
+
+ // build and sign CRL with CA private key
+ ContentSigner signer = new JcaContentSignerBuilder("SHA1WithRSA").setProvider(BC).build(caPrivateKey);
+ X509CRLHolder crl = crlBuilder.build(signer);
+
+ File tmpFile = new File(caRevocationList.getParentFile(), Long.toHexString(System.currentTimeMillis()) + ".tmp");
+ FileOutputStream fos = null;
+ try {
+ fos = new FileOutputStream(tmpFile);
+ fos.write(crl.getEncoded());
+ fos.flush();
+ fos.close();
+ if (caRevocationList.exists()) {
+ caRevocationList.delete();
+ }
+ tmpFile.renameTo(caRevocationList);
+
+ } finally {
+ if (fos != null) {
+ fos.close();
+ }
+ if (tmpFile.exists()) {
+ tmpFile.delete();
+ }
+ }
+
+ x509log.log(MessageFormat.format("Revoked certificate {0,number,0} reason: {1} [{2}]",
+ cert.getSerialNumber(), reason.toString(), cert.getSubjectDN().getName()));
+ return true;
+ } catch (Exception e) {
+ logger.error(MessageFormat.format("Failed to revoke certificate {0,number,0} [{1}] in {2}",
+ cert.getSerialNumber(), cert.getSubjectDN().getName(), caRevocationList));
+ }
+ return false;
+ }
+
+ /**
+ * Returns true if the certificate has been revoked.
+ *
+ * @param cert
+ * @param caRevocationList
+ * @return true if the certificate is revoked
+ */
+ public static boolean isRevoked(X509Certificate cert, File caRevocationList) {
+ if (!caRevocationList.exists()) {
+ return false;
+ }
+ InputStream inStream = null;
+ try {
+ inStream = new FileInputStream(caRevocationList);
+ CertificateFactory cf = CertificateFactory.getInstance("X.509");
+ X509CRL crl = (X509CRL)cf.generateCRL(inStream);
+ return crl.isRevoked(cert);
+ } catch (Exception e) {
+ logger.error(MessageFormat.format("Failed to check revocation status for certificate {0,number,0} [{1}] in {2}",
+ cert.getSerialNumber(), cert.getSubjectDN().getName(), caRevocationList));
+ } finally {
+ if (inStream != null) {
+ try {
+ inStream.close();
+ } catch (Exception e) {
+ }
+ }
+ }
+ return false;
+ }
+
+ public static X509Metadata getMetadata(X509Certificate cert) {
+ // manually split DN into OID components
+ // this is instead of parsing with LdapName which:
+ // (1) I don't trust the order of values
+ // (2) it filters out values like EMAILADDRESS
+ String dn = cert.getSubjectDN().getName();
+ Map<String, String> oids = new HashMap<String, String>();
+ for (String kvp : dn.split(",")) {
+ String [] val = kvp.trim().split("=");
+ String oid = val[0].toUpperCase().trim();
+ String data = val[1].trim();
+ oids.put(oid, data);
+ }
+
+ X509Metadata metadata = new X509Metadata(oids.get("CN"), "whocares");
+ metadata.oids.putAll(oids);
+ metadata.serialNumber = cert.getSerialNumber().toString();
+ metadata.notAfter = cert.getNotAfter();
+ metadata.notBefore = cert.getNotBefore();
+ metadata.emailAddress = metadata.getOID("E", null);
+ if (metadata.emailAddress == null) {
+ metadata.emailAddress = metadata.getOID("EMAILADDRESS", null);
+ }
+ return metadata;
+ }
+}