/*
 * Copyright (C) 2008, Shawn O. Pearce <spearce@spearce.org>
 * Copyright 2013 gitblit.com.
 * and other copyright owners as documented in the project's IP log.
 *
 * This program and the accompanying materials are made available
 * under the terms of the Eclipse Distribution License v1.0 which
 * accompanies this distribution, is reproduced below, and is
 * available at http://www.eclipse.org/org/documents/edl-v10.php
 *
 * All rights reserved.
 *
 * 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.servlet;

import java.awt.BasicStroke;
import java.awt.Color;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.RenderingHints;
import java.awt.Stroke;
import java.awt.image.BufferedImage;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;
import java.util.TreeSet;

import javax.imageio.ImageIO;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.eclipse.jgit.lib.Ref;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.revplot.AbstractPlotRenderer;
import org.eclipse.jgit.revplot.PlotCommit;
import org.eclipse.jgit.revplot.PlotCommitList;
import org.eclipse.jgit.revplot.PlotLane;
import org.eclipse.jgit.revplot.PlotWalk;
import org.eclipse.jgit.revwalk.RevCommit;

import com.gitblit.Constants;
import com.gitblit.IStoredSettings;
import com.gitblit.Keys;
import com.gitblit.dagger.DaggerServlet;
import com.gitblit.manager.IRepositoryManager;
import com.gitblit.utils.JGitUtils;
import com.gitblit.utils.StringUtils;

import dagger.ObjectGraph;

/**
 * Handles requests for branch graphs
 *
 * @author James Moger
 *
 */
public class BranchGraphServlet extends DaggerServlet {

	private static final long serialVersionUID = 1L;

	private static final int LANE_WIDTH = 14;

	// must match tr.commit css height
	private static final int ROW_HEIGHT = 24;

	private static final int RIGHT_PAD = 2;

	private final Stroke[] strokeCache;

	private IStoredSettings settings;

	private IRepositoryManager repositoryManager;

	public BranchGraphServlet() {
		super();
		strokeCache = new Stroke[4];
		for (int i = 1; i < strokeCache.length; i++) {
			strokeCache[i] = new BasicStroke(i);
		}
	}

	@Override
	protected void inject(ObjectGraph dagger) {
		this.settings = dagger.get(IStoredSettings.class);
		this.repositoryManager = dagger.get(IRepositoryManager.class);
	}

	/**
	 * Returns an url to this servlet for the specified parameters.
	 *
	 * @param baseURL
	 * @param repository
	 * @param objectId
	 * @param numberCommits
	 * @return an url
	 */
	public static String asLink(String baseURL, String repository, String objectId, int numberCommits) {
		if (baseURL.length() > 0 && baseURL.charAt(baseURL.length() - 1) == '/') {
			baseURL = baseURL.substring(0, baseURL.length() - 1);
		}
		return baseURL + Constants.BRANCH_GRAPH_PATH + "?r=" + repository
				+ (objectId == null ? "" : ("&h=" + objectId))
				+ (numberCommits > 0 ? ("&l=" + numberCommits) : "");
	}

	@Override
	protected long getLastModified(HttpServletRequest req) {
		String repository = req.getParameter("r");
		String objectId = req.getParameter("h");
		Repository r = null;
		try {
			r = repositoryManager.getRepository(repository);
			if (StringUtils.isEmpty(objectId)) {
				objectId = JGitUtils.getHEADRef(r);
			}
			RevCommit commit = JGitUtils.getCommit(r, objectId);
			return JGitUtils.getCommitDate(commit).getTime();
		} finally {
			if (r != null) {
				r.close();
			}
		}
	}

	@Override
	protected void doGet(HttpServletRequest request, HttpServletResponse response)
			throws ServletException, IOException {
		InputStream is = null;
		Repository r = null;
		PlotWalk rw = null;
		try {
			String repository = request.getParameter("r");
			String objectId = request.getParameter("h");
			String length = request.getParameter("l");

			r = repositoryManager.getRepository(repository);

			rw = new PlotWalk(r);
			if (StringUtils.isEmpty(objectId)) {
				objectId = JGitUtils.getHEADRef(r);
			}

			rw.markStart(rw.lookupCommit(r.resolve(objectId)));

			// default to the items-per-page setting, unless specified
			int maxCommits = settings.getInteger(Keys.web.itemsPerPage, 50);
			int requestedCommits = maxCommits;
			if (!StringUtils.isEmpty(length)) {
				int l = Integer.parseInt(length);
				if (l > 0) {
					requestedCommits = l;
				}
			}

			// fetch the requested commits plus some extra so that the last
			// commit displayed *likely* has correct lane assignments
			CommitList commitList = new CommitList();
			commitList.source(rw);
			commitList.fillTo(2*Math.max(requestedCommits, maxCommits));

			// determine the appropriate width for the image
			int numLanes = 1;
			int numCommits = Math.min(requestedCommits, commitList.size());
			if (numCommits > 1) {
				// determine graph width
				Set<String> parents = new TreeSet<String>();
				for (int i = 0; i < commitList.size(); i++) {
					PlotCommit<Lane> commit = commitList.get(i);
					boolean checkLane = false;

					if (i < numCommits) {
						// commit in visible list
						checkLane = true;

						// remember parents
						for (RevCommit p : commit.getParents()) {
							parents.add(p.getName());
						}
					} else if (parents.contains(commit.getName())) {
						// commit outside visible list, but it is a parent of a
						// commit in the visible list so we need to know it's lane
						// assignment
						checkLane = true;
					}

					if (checkLane) {
						int pos = commit.getLane().getPosition();
						numLanes = Math.max(numLanes, pos + 1);
					}
				}
			}

			int graphWidth = numLanes * LANE_WIDTH + RIGHT_PAD;
			int rowHeight = ROW_HEIGHT;

			// create an image buffer and render the lanes
			BufferedImage image = new BufferedImage(graphWidth, rowHeight*numCommits, BufferedImage.TYPE_INT_ARGB);

			Graphics2D g = null;
			try {
				g = image.createGraphics();
				g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
				LanesRenderer renderer = new LanesRenderer();
				for (int i = 0; i < commitList.size(); i++) {
					PlotCommit<Lane> commit = commitList.get(i);
					Graphics row = g.create(0, i*rowHeight, graphWidth, rowHeight);
					try {
						renderer.paint(row, commit, rowHeight, graphWidth);
					} finally {
						row.dispose();
						row = null;
					}
				}
			} finally {
				if (g != null) {
					g.dispose();
					g = null;
				}
			}

			// write the image buffer to the client
			response.setContentType("image/png");
			if (numCommits > 1) {
				response.setHeader("Cache-Control", "public, max-age=60, must-revalidate");
				response.setDateHeader("Last-Modified", JGitUtils.getCommitDate(commitList.get(0)).getTime());
			}
			OutputStream os = response.getOutputStream();
			ImageIO.write(image, "png", os);
			os.flush();
			image.flush();
			image = null;
		} catch (Exception e) {
			e.printStackTrace();
		} finally {
			if (is != null) {
				is.close();
				is = null;
			}
			if (rw != null) {
				rw.dispose();
				rw = null;
			}
			if (r != null) {
				r.close();
				r = null;
			}
		}
	}

	private Stroke stroke(final int width) {
		if (width < strokeCache.length)
			return strokeCache[width];
		return new BasicStroke(width);
	}

	static class CommitList extends PlotCommitList<Lane> {
		final List<Color> laneColors;
		final LinkedList<Color> colors;

		CommitList() {
			laneColors = new ArrayList<Color>();
			laneColors.add(new Color(133, 166, 214));
			laneColors.add(new Color(221, 205, 93));
			laneColors.add(new Color(199, 134, 57));
			laneColors.add(new Color(131, 150, 98));
			laneColors.add(new Color(197, 123, 127));
			laneColors.add(new Color(139, 136, 140));
			laneColors.add(new Color(48, 135, 144));
			laneColors.add(new Color(190, 93, 66));
			laneColors.add(new Color(143, 163, 54));
			laneColors.add(new Color(180, 148, 74));
			laneColors.add(new Color(101, 101, 217));
			laneColors.add(new Color(72, 153, 119));
			laneColors.add(new Color(23, 101, 160));
			laneColors.add(new Color(132, 164, 118));
			laneColors.add(new Color(255, 230, 59));
			laneColors.add(new Color(136, 176, 70));
			laneColors.add(new Color(255, 138, 1));
			laneColors.add(new Color(123, 187, 95));
			laneColors.add(new Color(233, 88, 98));
			laneColors.add(new Color(93, 158, 254));
			laneColors.add(new Color(175, 215, 0));
			laneColors.add(new Color(140, 134, 142));
			laneColors.add(new Color(232, 168, 21));
			laneColors.add(new Color(0, 172, 191));
			laneColors.add(new Color(251, 58, 4));
			laneColors.add(new Color(63, 64, 255));
			laneColors.add(new Color(27, 194, 130));
			laneColors.add(new Color(0, 104, 183));

			colors = new LinkedList<Color>();
			repackColors();
		}

		private void repackColors() {
			colors.addAll(laneColors);
		}

		@Override
		protected Lane createLane() {
			final Lane lane = new Lane();
			if (colors.isEmpty())
				repackColors();
			lane.color = colors.removeFirst();
			return lane;
		}

		@Override
		protected void recycleLane(final Lane lane) {
			colors.add(lane.color);
		}
	}

	static class Lane extends PlotLane {

		private static final long serialVersionUID = 1L;

		Color color;

		@Override
		public boolean equals(Object o) {
			return super.equals(o) && color.equals(((Lane)o).color);
		}

		@Override
		public int hashCode() {
			return super.hashCode() ^ color.hashCode();
		}
	}

	class LanesRenderer extends AbstractPlotRenderer<Lane, Color> implements Serializable {

		private static final long serialVersionUID = 1L;

		final Color commitDotFill = new Color(220, 220, 220);

		final Color commitDotOutline = new Color(110, 110, 110);

		transient Graphics2D g;

		void paint(Graphics in, PlotCommit<Lane> commit, int h, int w) {
			g = (Graphics2D) in.create();
			try {
				if (commit != null)
					paintCommit(commit, h);
			} finally {
				g.dispose();
				g = null;
			}
		}

		@Override
		protected void drawLine(Color color, int x1, int y1, int x2, int y2, int width) {
			if (y1 == y2) {
				x1 -= width / 2;
				x2 -= width / 2;
			} else if (x1 == x2) {
				y1 -= width / 2;
				y2 -= width / 2;
			}

			g.setColor(color);
			g.setStroke(stroke(width));
			g.drawLine(x1, y1, x2, y2);
		}

		@Override
		protected void drawCommitDot(int x, int y, int w, int h) {
			g.setColor(commitDotFill);
			g.setStroke(strokeCache[2]);
			g.fillOval(x + 2, y + 1, w - 2, h - 2);
			g.setColor(commitDotOutline);
			g.drawOval(x + 2, y + 1, w - 2, h - 2);
		}

		@Override
		protected void drawBoundaryDot(int x, int y, int w, int h) {
			drawCommitDot(x, y, w, h);
		}

		@Override
		protected void drawText(String msg, int x, int y) {
		}

		@Override
		protected Color laneColor(Lane myLane) {
			return myLane != null ? myLane.color : Color.black;
		}

		@Override
		protected int drawLabel(int x, int y, Ref ref) {
			return 0;
		}
	}
}
4</a>
<a id='n115' href='#n115'>115</a>
<a id='n116' href='#n116'>116</a>
<a id='n117' href='#n117'>117</a>
<a id='n118' href='#n118'>118</a>
<a id='n119' href='#n119'>119</a>
<a id='n120' href='#n120'>120</a>
<a id='n121' href='#n121'>121</a>
<a id='n122' href='#n122'>122</a>
<a id='n123' href='#n123'>123</a>
<a id='n124' href='#n124'>124</a>
<a id='n125' href='#n125'>125</a>
<a id='n126' href='#n126'>126</a>
<a id='n127' href='#n127'>127</a>
<a id='n128' href='#n128'>128</a>
<a id='n129' href='#n129'>129</a>
<a id='n130' href='#n130'>130</a>
<a id='n131' href='#n131'>131</a>
<a id='n132' href='#n132'>132</a>
<a id='n133' href='#n133'>133</a>
<a id='n134' href='#n134'>134</a>
<a id='n135' href='#n135'>135</a>
<a id='n136' href='#n136'>136</a>
<a id='n137' href='#n137'>137</a>
<a id='n138' href='#n138'>138</a>
<a id='n139' href='#n139'>139</a>
<a id='n140' href='#n140'>140</a>
<a id='n141' href='#n141'>141</a>
<a id='n142' href='#n142'>142</a>
<a id='n143' href='#n143'>143</a>
<a id='n144' href='#n144'>144</a>
<a id='n145' href='#n145'>145</a>
</pre></td>
<td class='lines'><pre><code><style>pre { line-height: 125%; }
td.linenos .normal { color: inherit; background-color: transparent; padding-left: 5px; padding-right: 5px; }
span.linenos { color: inherit; background-color: transparent; padding-left: 5px; padding-right: 5px; }
td.linenos .special { color: #000000; background-color: #ffffc0; padding-left: 5px; padding-right: 5px; }
span.linenos.special { color: #000000; background-color: #ffffc0; padding-left: 5px; padding-right: 5px; }
.highlight .hll { background-color: #ffffcc }
.highlight .c { color: #888888 } /* Comment */
.highlight .err { color: #a61717; background-color: #e3d2d2 } /* Error */
.highlight .k { color: #008800; font-weight: bold } /* Keyword */
.highlight .ch { color: #888888 } /* Comment.Hashbang */
.highlight .cm { color: #888888 } /* Comment.Multiline */
.highlight .cp { color: #cc0000; font-weight: bold } /* Comment.Preproc */
.highlight .cpf { color: #888888 } /* Comment.PreprocFile */
.highlight .c1 { color: #888888 } /* Comment.Single */
.highlight .cs { color: #cc0000; font-weight: bold; background-color: #fff0f0 } /* Comment.Special */
.highlight .gd { color: #000000; background-color: #ffdddd } /* Generic.Deleted */
.highlight .ge { font-style: italic } /* Generic.Emph */
.highlight .gr { color: #aa0000 } /* Generic.Error */
.highlight .gh { color: #333333 } /* Generic.Heading */
.highlight .gi { color: #000000; background-color: #ddffdd } /* Generic.Inserted */
.highlight .go { color: #888888 } /* Generic.Output */
.highlight .gp { color: #555555 } /* Generic.Prompt */
.highlight .gs { font-weight: bold } /* Generic.Strong */
.highlight .gu { color: #666666 } /* Generic.Subheading */
.highlight .gt { color: #aa0000 } /* Generic.Traceback */
.highlight .kc { color: #008800; font-weight: bold } /* Keyword.Constant */
.highlight .kd { color: #008800; font-weight: bold } /* Keyword.Declaration */
.highlight .kn { color: #008800; font-weight: bold } /* Keyword.Namespace */
.highlight .kp { color: #008800 } /* Keyword.Pseudo */
.highlight .kr { color: #008800; font-weight: bold } /* Keyword.Reserved */
.highlight .kt { color: #888888; font-weight: bold } /* Keyword.Type */
.highlight .m { color: #0000DD; font-weight: bold } /* Literal.Number */
.highlight .s { color: #dd2200; background-color: #fff0f0 } /* Literal.String */
.highlight .na { color: #336699 } /* Name.Attribute */
.highlight .nb { color: #003388 } /* Name.Builtin */
.highlight .nc { color: #bb0066; font-weight: bold } /* Name.Class */
.highlight .no { color: #003366; font-weight: bold } /* Name.Constant */
.highlight .nd { color: #555555 } /* Name.Decorator */
.highlight .ne { color: #bb0066; font-weight: bold } /* Name.Exception */
.highlight .nf { color: #0066bb; font-weight: bold } /* Name.Function */
.highlight .nl { color: #336699; font-style: italic } /* Name.Label */
.highlight .nn { color: #bb0066; font-weight: bold } /* Name.Namespace */
.highlight .py { color: #336699; font-weight: bold } /* Name.Property */
.highlight .nt { color: #bb0066; font-weight: bold } /* Name.Tag */
.highlight .nv { color: #336699 } /* Name.Variable */
.highlight .ow { color: #008800 } /* Operator.Word */
.highlight .w { color: #bbbbbb } /* Text.Whitespace */
.highlight .mb { color: #0000DD; font-weight: bold } /* Literal.Number.Bin */
.highlight .mf { color: #0000DD; font-weight: bold } /* Literal.Number.Float */
.highlight .mh { color: #0000DD; font-weight: bold } /* Literal.Number.Hex */
.highlight .mi { color: #0000DD; font-weight: bold } /* Literal.Number.Integer */
.highlight .mo { color: #0000DD; font-weight: bold } /* Literal.Number.Oct */
.highlight .sa { color: #dd2200; background-color: #fff0f0 } /* Literal.String.Affix */
.highlight .sb { color: #dd2200; background-color: #fff0f0 } /* Literal.String.Backtick */
.highlight .sc { color: #dd2200; background-color: #fff0f0 } /* Literal.String.Char */
.highlight .dl { color: #dd2200; background-color: #fff0f0 } /* Literal.String.Delimiter */
.highlight .sd { color: #dd2200; background-color: #fff0f0 } /* Literal.String.Doc */
.highlight .s2 { color: #dd2200; background-color: #fff0f0 } /* Literal.String.Double */
.highlight .se { color: #0044dd; background-color: #fff0f0 } /* Literal.String.Escape */
.highlight .sh { color: #dd2200; background-color: #fff0f0 } /* Literal.String.Heredoc */
.highlight .si { color: #3333bb; background-color: #fff0f0 } /* Literal.String.Interpol */
.highlight .sx { color: #22bb22; background-color: #f0fff0 } /* Literal.String.Other */
.highlight .sr { color: #008800; background-color: #fff0ff } /* Literal.String.Regex */
.highlight .s1 { color: #dd2200; background-color: #fff0f0 } /* Literal.String.Single */
.highlight .ss { color: #aa6600; background-color: #fff0f0 } /* Literal.String.Symbol */
.highlight .bp { color: #003388 } /* Name.Builtin.Pseudo */
.highlight .fm { color: #0066bb; font-weight: bold } /* Name.Function.Magic */
.highlight .vc { color: #336699 } /* Name.Variable.Class */
.highlight .vg { color: #dd7700 } /* Name.Variable.Global */
.highlight .vi { color: #3333bb } /* Name.Variable.Instance */
.highlight .vm { color: #336699 } /* Name.Variable.Magic */
.highlight .il { color: #0000DD; font-weight: bold } /* Literal.Number.Integer.Long */</style><div class="highlight"><pre><span></span>---
title: Handling Errors
order: 6
layout: page
---

[[application.errors]]
= Handling Errors

[[application.errors.error-indicator]]
== Error Indicator and Message

All components have a built-in error indicator that is turned on if validating
the component fails, and can be set explicitly with
[methodname]#setComponentError()#. Usually, the error indicator is placed right
of the component caption. The error indicator is part of the component caption,
so its placement is usually managed by the layout in which the component is
contained, but some components handle it themselves. Hovering the mouse pointer
over the field displays the error message.


[source, java]
----
textfield.setComponentError(new UserError(&quot;Bad value&quot;));
button.setComponentError(new UserError(&quot;Bad click&quot;));
----

The result is shown in &lt;&lt;figure.application.errors.error-indicator&gt;&gt;.

[[figure.application.errors.error-indicator]]
.Error Indicator Active
image::img/errorindicator-example2.png[scaledwidth=40%]

[[application.errors.reconnect]]
== Connection Fault

If the connection to the server is lost, Vaadin application shows a &quot;lost connection&quot; notification
and tries to restore the connection. After several retries, an error message is shown.
You can customize the messages, timeouts, and the number of reconnect attempts in the [classname]#ReconnectDialogConfiguration#
object, which you can access from your [classname]#UI# with [methodname]#getReconnectDialogConfiguration()#.

[[application.errors.systemmessages]]
== Customizing System Messages

System messages are notifications that indicate a major invalid state that
usually requires restarting the application. Session timeout is perhaps the most
typical such state.

System messages are strings managed in the [classname]#SystemMessages# class.
Each message has four properties: a short caption, the actual message, a URL to
which to redirect after displaying the message, and property indicating whether
the notification is enabled.

You can override the default system messages by setting the
[interfacename]#SystemMessagesProvider# in the [classname]#VaadinService#. You
need to implement the [methodname]#getSystemMessages()# method, which should
return a [classname]#SystemMessages# object. The easiest way to customize the
messages is to use a [classname]#CustomizedSystemMessages# object.

You can set the system message provider in the
[methodname]#servletInitialized()# method of a custom servlet class, for example
as follows:


[source, java]
----
getService().setSystemMessagesProvider(
    new SystemMessagesProvider() {
    @Override
    public SystemMessages getSystemMessages(
        SystemMessagesInfo systemMessagesInfo) {
        CustomizedSystemMessages messages =
                new CustomizedSystemMessages();
        messages.setCommunicationErrorCaption(&quot;Comm Err&quot;);
        messages.setCommunicationErrorMessage(&quot;This is bad.&quot;);
        messages.setCommunicationErrorNotificationEnabled(true);
        messages.setCommunicationErrorURL(&quot;http://vaadin.com/&quot;);
        return messages;
    }
});
----

See
&lt;&lt;dummy/../../../framework/application/application-lifecycle#application.lifecycle.servlet-service,&quot;Vaadin
Servlet, Portlet, and Service&quot;&gt;&gt; for information about customizing Vaadin
servlets.

[[application.errors.unchecked-exceptions]]
== Handling Uncaught Exceptions

Handling events can result in exceptions either in the application logic or in
the framework itself, but some of them may not be caught properly by the
application. Any such exceptions are eventually caught by the framework. It
delegates the exceptions to the [classname]#DefaultErrorHandler#, which displays
the error as a component error, that is, with a small red &quot;!&quot; -sign (depending
on the theme). If the user hovers the mouse pointer over it, the entire
backtrace of the exception is shown in a large tooltip box.


You can customize the default error handling by implementing a custom
[interfacename]#ErrorHandler# and enabling it with
[methodname]#setErrorHandler()# in any of the components in the component
hierarchy, including the [classname]#UI#, or in the [classname]#VaadinSession#
object. You can either implement the [interfacename]#ErrorHandler# or extend the
[classname]#DefaultErrorHandler#. In the following example, we modify the
behavior of the default handler.


[source, java]
----
// Here&#39;s some code that produces an uncaught exception
final VerticalLayout layout = new VerticalLayout();
final Button button = new Button(&quot;Click Me!&quot;, event -&gt;
        ((String)null).length()); // Null-pointer exception
layout.addComponent(button);

// Configure the error handler for the UI
UI.getCurrent().setErrorHandler(new DefaultErrorHandler() {
    @Override
    public void error(com.vaadin.server.ErrorEvent event) {
        // Find the final cause
        String cause = &quot;&lt;b&gt;The click failed because:&lt;/b&gt;&lt;br/&gt;&quot;;
        for (Throwable t = event.getThrowable(); t != null;
             t = t.getCause())
            if (t.getCause() == null) // We&#39;re at final cause
                cause += t.getClass().getName() + &quot;&lt;br/&gt;&quot;;

        // Display the error message in a custom fashion
        layout.addComponent(new Label(cause, ContentMode.HTML));

        // Do the default error handling (optional)
        doDefault(event);
    }
});
----

The above example also demonstrates how to dig up the final cause from the cause
stack.

When extending [classname]#DefaultErrorHandler#, you can call
[methodname]#doDefault()# as was done above to run the default error handling,
such as set the component error for the component where the exception was
thrown. See the source code of the implementation for more details. You can call
[methodname]#findAbstractComponent(event)# to find the component that caused the
error. If the error is not associated with a component, it returns null.
</pre></div>
</code></pre></td></tr></table>
</div> <!-- class=content -->
<div class='footer'>generated by <a href='https://git.zx2c4.com/cgit/about/'>cgit v1.2.3</a> (<a href='https://git-scm.com/'>git 2.39.1</a>) at 2025-08-10 12:14:55 +0000</div>
</div> <!-- id=cgit -->
</body>
</html>
