feature: implements mid trimming long text; implements repo length truncation

This commit is contained in:
ievgenii vdovenko 2026-01-06 17:54:17 +01:00
parent 1906b1960f
commit e0b2043c71
21 changed files with 311 additions and 96 deletions

View file

@ -70,6 +70,7 @@
"<%= dirs.js.src_rc %>/i18n/select2/translations.js", "<%= dirs.js.src_rc %>/i18n/select2/translations.js",
"<%= dirs.js.src %>/rhodecode/utils/array.js", "<%= dirs.js.src %>/rhodecode/utils/array.js",
"<%= dirs.js.src %>/rhodecode/utils/string.js", "<%= dirs.js.src %>/rhodecode/utils/string.js",
"<%= dirs.js.src %>/rhodecode/utils/trimText.js",
"<%= dirs.js.src %>/rhodecode/utils/pyroutes.js", "<%= dirs.js.src %>/rhodecode/utils/pyroutes.js",
"<%= dirs.js.src %>/rhodecode/utils/ajax.js", "<%= dirs.js.src %>/rhodecode/utils/ajax.js",
"<%= dirs.js.src %>/rhodecode/utils/autocomplete.js", "<%= dirs.js.src %>/rhodecode/utils/autocomplete.js",

View file

@ -2452,3 +2452,19 @@ def get_directory_statistics(start_path):
total_size += dir_size total_size += dir_size
return total_files, total_size, directory_stats return total_files, total_size, directory_stats
def truncate_middle(text_, max_length=100):
ellipsis = "..."
min_limit = 5
if len(text_) <= max_length:
return text_
max_length = max(min_limit, max_length)
keep = max_length - len(ellipsis)
left = math.ceil(keep / 2)
right = math.ceil(keep / 2)
return text_[:left] + ellipsis + text_[-right:]

View file

@ -113,7 +113,7 @@ def get_diff_info(source_repo, source_ref, target_repo, target_ref, get_authors=
target_scm = target_repo.scm_instance() target_scm = target_repo.scm_instance()
ancestor_id = target_scm.get_common_ancestor(target_ref, source_ref, source_scm) ancestor_id = target_scm.get_common_ancestor(target_ref, source_ref, source_scm)
if not ancestor_id: if not ancestor_id or (isinstance(ancestor_id, str) and ancestor_id.lower() == "none"):
raise ValueError( raise ValueError(
"cannot calculate diff info without a common ancestor. " "cannot calculate diff info without a common ancestor. "
"Make sure both repositories are related, and have a common forking commit." "Make sure both repositories are related, and have a common forking commit."

View file

@ -208,6 +208,10 @@
.fieldset { .fieldset {
pre {
overflow-x: auto;
}
.left-label { // similar to form legend .left-label { // similar to form legend
display: block; display: block;
margin: 0; margin: 0;

View file

@ -186,7 +186,6 @@ table.dataTable {
&.truncate, .truncate-wrap { &.truncate, .truncate-wrap {
white-space: nowrap; white-space: nowrap;
overflow: hidden; overflow: hidden;
text-overflow: ellipsis;
max-width: 350px; max-width: 350px;
} }
} }
@ -345,9 +344,6 @@ table.dataTable {
max-width: 450px; max-width: 450px;
width: 300px; width: 300px;
overflow: hidden; overflow: hidden;
text-overflow: ellipsis;
-o-text-overflow: ellipsis;
-ms-text-overflow: ellipsis;
&.autoexpand { &.autoexpand {
width: 120px; width: 120px;

View file

@ -333,7 +333,7 @@ var tooltipActivate = function () {
}; };
// Formatting values in a Select2 dropdown of commit references // Formatting values in a Select2 dropdown of commit references
var formatSelect2SelectionRefs = function(commit_ref){ var formatSelect2SelectionRefs = function(commit_ref, tripTextFn){
var tmpl = ''; var tmpl = '';
if (!commit_ref.text || commit_ref.type === 'sha'){ if (!commit_ref.text || commit_ref.type === 'sha'){
return commit_ref.text; return commit_ref.text;
@ -345,6 +345,10 @@ var formatSelect2SelectionRefs = function(commit_ref){
} else if (commit_ref.type === 'book'){ } else if (commit_ref.type === 'book'){
tmpl = tmpl.concat('<i class="icon-bookmark"></i> '); tmpl = tmpl.concat('<i class="icon-bookmark"></i> ');
} }
if (tripTextFn !== undefined) {
return tmpl.concat(tripTextFn(escapeHtml(commit_ref.text)));
}
return tmpl.concat(escapeHtml(commit_ref.text)); return tmpl.concat(escapeHtml(commit_ref.text));
}; };

View file

@ -0,0 +1,153 @@
const DEFAULT_LIMIT = 30; // used only if auto can't compute
const MIN_LIMIT = 5;
const ELLIPSIS = "...";
const FILL = 0.90; // use 90% of available width
// Cache average char width per font+letterSpacing
const avgCharWidthCache = new Map();
function toChars(s) {
return Array.from(s);
} // unicode-safe
function truncateMiddle(str, maxLen) {
maxLen = parseInt(maxLen, 10);
if (!Number.isFinite(maxLen)) maxLen = DEFAULT_LIMIT;
maxLen = Math.max(MIN_LIMIT, maxLen);
const full = (str || "").trim();
const chars = toChars(full);
if (chars.length <= maxLen) return full;
const keep = maxLen - ELLIPSIS.length; // remaining visible chars besides "..."
const left = Math.ceil(keep / 2);
const right = Math.floor(keep / 2);
return chars.slice(0, left).join("") + ELLIPSIS + chars.slice(chars.length - right).join("");
}
function ensureTextSpan($a) {
let $span = $a.find("span.js-midcut").first();
if ($span.length) return $span;
const textNodes = $a.contents().filter(function () {
return this.nodeType === 3 && this.nodeValue && this.nodeValue.trim().length > 0;
});
if (!textNodes.length) return null;
const full = textNodes.map(function () {
return this.nodeValue;
}).get().join(" ").trim();
textNodes.remove();
$span = $("<span/>", {class: "js-midcut", text: full})
.attr("data-full", full)
.attr("title", full);
$a.append($span);
return $span;
}
function getAvgCharWidthPx(el) {
const cs = window.getComputedStyle(el);
// Include letter-spacing because it affects width
const key = [
cs.fontStyle, cs.fontVariant, cs.fontWeight,
cs.fontSize, cs.fontFamily, cs.letterSpacing
].join("|");
if (avgCharWidthCache.has(key)) return avgCharWidthCache.get(key);
// Measure using a hidden span with same font + letter spacing
const sample = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
const repeat = 6; // 62*6 = 372 chars -> stable average
const text = sample.repeat(repeat);
const meas = document.createElement("span");
meas.style.position = "absolute";
meas.style.visibility = "hidden";
meas.style.whiteSpace = "nowrap";
meas.style.left = "-99999px";
meas.style.top = "-99999px";
meas.style.fontStyle = cs.fontStyle;
meas.style.fontVariant = cs.fontVariant;
meas.style.fontWeight = cs.fontWeight;
meas.style.fontSize = cs.fontSize;
meas.style.fontFamily = cs.fontFamily;
meas.style.letterSpacing = cs.letterSpacing;
meas.textContent = text;
document.body.appendChild(meas);
const width = meas.getBoundingClientRect().width;
document.body.removeChild(meas);
const avg = width / text.length || 8; // fallback if something weird
avgCharWidthCache.set(key, avg);
return avg;
}
// Auto maxLen from width (px) → chars, using 90% available width
function autoMaxLenForAnchor($a) {
const aEl = $a[0];
const $truncate = $a.closest(".truncate");
const containerEl = $truncate[0] || aEl;
const containerW = containerEl.getBoundingClientRect().width;
if (!containerW) return null;
// Subtract widths of non-text children (icons etc.), exclude our span
let nonTextW = 0;
$a.children().not("span.js-midcut").each(function () {
nonTextW += $(this).outerWidth(true) || 0;
});
const availablePx = Math.max(0, (containerW - nonTextW) * FILL);
if (!availablePx) return null;
const avgCharW = getAvgCharWidthPx(aEl);
if (!avgCharW) return null;
const est = Math.floor(availablePx / avgCharW);
return Math.max(MIN_LIMIT, est);
}
// limitOverride: optional manual max length for all elements in this call
function applyMiddleCut(selector, limitOverride) {
$(selector).each(function () {
const $el = $(this);
const $span = ensureTextSpan($el);
if (!$span) return;
const full = $span.attr("data-full") || $span.text();
// 1) per-element override (if you can add attributes)
// <div class="truncate" data-maxlen="20"> OR <a data-maxlen="20">
const perEl =
$el.closest(".truncate").attr("data-maxlen") ||
$el.attr("data-maxlen");
// 2) manual override passed to function
// 3) auto-detected from width
// 4) fallback default
const autoLen = autoMaxLenForAnchor($el);
const limit = perEl || limitOverride || autoLen || DEFAULT_LIMIT;
const trimmed = truncateMiddle(full, limit);
if ($span.text() !== trimmed) $span.text(trimmed);
});
}
function trimRepoTitleName() {
const $repoNameTitle = $("#repo-name-title");
let text = $repoNameTitle.text();
let arr = text.split(": ");
if (arr.length < 2) {
return;
}
let repoName = arr[arr.length - 1];
let symbolsCnt = autoMaxLenForAnchor($repoNameTitle);
const trimmedName = truncateMiddle(repoName, symbolsCnt);
$repoNameTitle.text(arr[0] + ": " + trimmedName);
}

View file

@ -37,8 +37,8 @@
</%def> </%def>
<div class="panel panel-default"> <div class="panel panel-default">
<div class="panel-heading"> <div class="panel-heading truncate">
<h3 class="panel-title"> <h3 class="panel-title" id="repo-name-title">
%if c.repo: %if c.repo:
${_('Current Integrations for Repository: {repo_name}').format(repo_name=c.repo.repo_name)} ${_('Current Integrations for Repository: {repo_name}').format(repo_name=c.repo.repo_name)}
%elif c.repo_group: %elif c.repo_group:
@ -81,7 +81,7 @@
<td colspan="7"> <td colspan="7">
%if c.repo: %if c.repo:
${_('No {type} integrations for repo {repo} exist yet.').format(type=integration_type, repo=c.repo.repo_name)} ${_('No {type} integrations for repo {repo} exist yet.').format(type=integration_type, repo=h.truncate_middle(c.repo.repo_name))}
%elif c.repo_group: %elif c.repo_group:
${_('No {type} integrations for repogroup {repogroup} exist yet.').format(type=integration_type, repogroup=c.repo_group.group_name)} ${_('No {type} integrations for repogroup {repogroup} exist yet.').format(type=integration_type, repogroup=c.repo_group.group_name)}
%else: %else:
@ -91,7 +91,7 @@
%if c.current_IntegrationType: %if c.current_IntegrationType:
<% <%
if c.repo: if c.repo:
create_url = h.route_path('repo_integrations_create', repo_name=c.repo.repo_name, integration=c.current_IntegrationType.key) create_url = h.route_path('repo_integrations_create', repo_name=h.truncate_middle(c.repo.repo_name), integration=c.current_IntegrationType.key)
elif c.repo_group: elif c.repo_group:
create_url = h.route_path('repo_group_integrations_create', repo_group_name=c.repo_group.group_name, integration=c.current_IntegrationType.key) create_url = h.route_path('repo_group_integrations_create', repo_group_name=c.repo_group.group_name, integration=c.current_IntegrationType.key)
else: else:
@ -128,7 +128,7 @@
<td class="td-scope"> <td class="td-scope">
%if integration.repo: %if integration.repo:
<a href="${h.route_path('repo_summary', repo_name=integration.repo.repo_name)}"> <a href="${h.route_path('repo_summary', repo_name=integration.repo.repo_name)}">
${_('repo')}:${integration.repo.repo_name} ${_('repo')}:${h.truncate_middle(integration.repo.repo_name)}
</a> </a>
%elif integration.repo_group: %elif integration.repo_group:
<a href="${h.route_path('repo_group_home', repo_group_name=integration.repo_group.group_name)}"> <a href="${h.route_path('repo_group_home', repo_group_name=integration.repo_group.group_name)}">
@ -216,4 +216,5 @@
e.preventDefault(); e.preventDefault();
delete_integration(this); delete_integration(this);
}); });
trimRepoTitleName();
</script> </script>

View file

@ -19,7 +19,8 @@ ${h.secure_form(h.route_path('repo_create'), request=request)}
${h.select('repo_group', request.GET.get('parent_group'), c.repo_groups, class_="medium")} ${h.select('repo_group', request.GET.get('parent_group'), c.repo_groups, class_="medium")}
<div class="repo-create-spacer"> / </div> <div class="repo-create-spacer"> / </div>
<div class="repo-name-input-container"> <div class="repo-name-input-container">
${h.text('repo_name', class_="medium", id="repo_name_input")} ${h.text('repo_name', class_="medium", id="repo_name_input", maxlength=192)}
<div id="repo_name_error" class="error" aria-live="polite" style="display:none; color: #c00; margin-top: 6px; font-size: 0.9rem;"></div>
<div class="repo-check-container check-progress" id="repo_check" style="display: none"></div> <div class="repo-check-container check-progress" id="repo_check" style="display: none"></div>
</div> </div>
</div> </div>
@ -282,6 +283,7 @@ ${h.secure_form(h.route_path('repo_create'), request=request)}
} }
}) })
}) trimRepoTitleName();
});
</script> </script>
${h.end_form()} ${h.end_form()}

View file

@ -17,8 +17,8 @@
%> %>
<div class="panel panel-default"> <div class="panel panel-default">
<div class="panel-heading" id="advanced-info" > <div class="panel-heading truncate" id="advanced-info" >
<h3 class="panel-title">${_('Repository: %s') % c.rhodecode_db_repo.repo_name} <a class="permalink" href="#advanced-info"> ¶</a></h3> <h3 class="panel-title" id="repo-name-title">${_('Repository: %s') % c.rhodecode_db_repo.repo_name} <a class="permalink" href="#advanced-info"> ¶</a></h3>
</div> </div>
<div class="panel-body"> <div class="panel-body">
${base.dt_info_panel(elems)} ${base.dt_info_panel(elems)}
@ -241,7 +241,7 @@
<div class="field"> <div class="field">
<button class="btn btn-small btn-danger" type="submit" <button class="btn btn-small btn-danger" type="submit"
onclick="submitConfirm(event, this, _gettext('Confirm to delete this repository'), _gettext('Delete'), '${c.rhodecode_db_repo.repo_name}')" onclick="submitConfirm(event, this, _gettext('Confirm to delete this repository'), _gettext('Delete'), '${h.truncate_middle(c.rhodecode_db_repo.repo_name, 50)}')"
> >
${_('Delete this repository')} ${_('Delete this repository')}
</button> </button>
@ -263,7 +263,6 @@
<script> <script>
var currentRepoId = ${c.rhodecode_db_repo.repo_id}; var currentRepoId = ${c.rhodecode_db_repo.repo_id};
var repoTypeFilter = function(data) { var repoTypeFilter = function(data) {
@ -330,6 +329,6 @@ function syncHelpVisibility() {
$checkbox.on('change', syncHelpVisibility) $checkbox.on('change', syncHelpVisibility)
syncHelpVisibility(); syncHelpVisibility();
trimRepoTitleName();
</script> </script>

View file

@ -21,3 +21,7 @@
</div> </div>
</div> </div>
<script>
applyMiddleCut('.rctable .td-componentname a, .rctable .td-componentname', 70);
</script>

View file

@ -1,8 +1,8 @@
<%namespace name="base" file="/base/base.mako"/> <%namespace name="base" file="/base/base.mako"/>
<div class="panel panel-default"> <div class="panel panel-default">
<div class="panel-heading"> <div class="panel-heading truncate">
<h3 class="panel-title">${_('Settings for Repository: %s') % c.rhodecode_db_repo.repo_name}</h3> <h3 id="repo-name-title" class="panel-title">${_('Settings for Repository: %s') % c.rhodecode_db_repo.repo_name}</h3>
</div> </div>
<div class="panel-body"> <div class="panel-body">
${h.secure_form(h.route_path('edit_repo', repo_name=c.rhodecode_db_repo.repo_name), request=request)} ${h.secure_form(h.route_path('edit_repo', repo_name=c.rhodecode_db_repo.repo_name), request=request)}
@ -332,5 +332,7 @@
}; };
UsersAutoComplete('repo_owner', '${c.rhodecode_user.user_id}'); UsersAutoComplete('repo_owner', '${c.rhodecode_user.user_id}');
trimRepoTitleName();
}); });
</script> </script>

View file

@ -141,6 +141,10 @@ $(document).ready(function() {
}) })
); );
$(document).on("draw.dt", function () {
applyMiddleCut('#repo_list_table .truncate a', 60);
applyMiddleCut('.truncate-wrap');
});
}); });
</script> </script>

View file

@ -854,7 +854,7 @@
var tmpl = ''; var tmpl = '';
var repoType = data['repo_type']; var repoType = data['repo_type'];
var repoName = data['text']; var repoName = truncateMiddle(data['text'], 60);
if(data && data.type == 'repo'){ if(data && data.type == 'repo'){
if(repoType === 'hg'){ if(repoType === 'hg'){
@ -1222,6 +1222,9 @@
window.updateStickyHeader(); window.updateStickyHeader();
} }
} }
})() $(function () {
applyMiddleCut('#context-bar .title a');
});
})();
</script> </script>

View file

@ -219,7 +219,7 @@
var enable_fields = ${"false" if c.preview_mode else "true"}; var enable_fields = ${"false" if c.preview_mode else "true"};
$("#compare_source").select2({ $("#compare_source").select2({
placeholder: "${'%s@%s' % (c.source_repo.repo_name, c.source_ref)}", placeholder: "${'%s@%s' % (h.truncate_middle(c.source_repo.repo_name), c.source_ref)}",
containerCssClass: "drop-menu", containerCssClass: "drop-menu",
dropdownCssClass: "drop-menu-dropdown", dropdownCssClass: "drop-menu-dropdown",
formatSelection: formatSelection("${c.source_repo.repo_name}"), formatSelection: formatSelection("${c.source_repo.repo_name}"),
@ -238,7 +238,7 @@
}).select2("enable", enable_fields); }).select2("enable", enable_fields);
$("#compare_target").select2({ $("#compare_target").select2({
placeholder: "${'%s@%s' % (c.target_repo.repo_name, c.target_ref)}", placeholder: "${'%s@%s' % (h.truncate_middle(c.target_repo.repo_name), c.target_ref)}",
dropdownAutoWidth: true, dropdownAutoWidth: true,
containerCssClass: "drop-menu", containerCssClass: "drop-menu",
dropdownCssClass: "drop-menu-dropdown", dropdownCssClass: "drop-menu-dropdown",

View file

@ -402,10 +402,10 @@ ${h.style_metatag(tag_type, tag)|n,trim}
%> %>
## Source ## Source
<code class="pr-source-info"><i class="icon-${pr_ref_type_to_icon(pull_request.source_ref_parts.type)}"></i>${pull_request.source_ref_parts.name}</code> <code class="pr-source-info"><i class="icon-${pr_ref_type_to_icon(pull_request.source_ref_parts.type)}"></i>${h.truncate_middle(pull_request.source_ref_parts.name, 70)}</code>
&rarr; &rarr;
## Target ## Target
<code class="pr-target-info"><i class="icon-${pr_ref_type_to_icon(pull_request.target_ref_parts.type)}"></i>${pull_request.target_ref_parts.name}</code> <code class="pr-target-info"><i class="icon-${pr_ref_type_to_icon(pull_request.target_ref_parts.type)}"></i>${h.truncate_middle(pull_request.target_ref_parts.name, 90)}</code>
</div> </div>
</%def> </%def>

View file

@ -286,9 +286,17 @@
query.callback(data); query.callback(data);
}; };
const truncateSelection = function(item) {
return truncateMiddle(item, 35);
}
const truncateResult = function(item) {
return truncateMiddle(item, 60);
}
var select2RefFileSwitcher = function (targetElement, loadUrl, initialData) { var select2RefFileSwitcher = function (targetElement, loadUrl, initialData) {
var formatResult = function (result, container, query) { var formatResult = function (result, container, query) {
return formatSelect2SelectionRefs(result); return formatSelect2SelectionRefs(result, truncateResult);
}; };
var formatSelection = function (data, container) { var formatSelection = function (data, container) {
@ -299,7 +307,7 @@
tmpl = (commit_ref.raw_id || "").substr(0,8); tmpl = (commit_ref.raw_id || "").substr(0,8);
} else if (commit_ref.type === 'branch') { } else if (commit_ref.type === 'branch') {
tmpl = tmpl.concat('<i class="icon-branch"></i> '); tmpl = tmpl.concat('<i class="icon-branch"></i> ');
tmpl = tmpl.concat(escapeHtml(commit_ref.text)); tmpl = tmpl.concat(truncateSelection(escapeHtml(commit_ref.text)));
} else if (commit_ref.type === 'tag') { } else if (commit_ref.type === 'tag') {
tmpl = tmpl.concat('<i class="icon-tag"></i> '); tmpl = tmpl.concat('<i class="icon-tag"></i> ');
tmpl = tmpl.concat(escapeHtml(commit_ref.text)); tmpl = tmpl.concat(escapeHtml(commit_ref.text));

View file

@ -10,6 +10,7 @@
</div> </div>
${h.hidden('refs_filter')} ${h.hidden('refs_filter')}
<!-- ////-->
<div class="info_box_elem next"> <div class="info_box_elem next">
<a id="next_commit_link" data-commit-id="${c.next_commit.raw_id}" class=" ${('disabled' if c.url_next == '#' else '')}" href="${c.url_next}" title="${_('Next commit')}"><i class="icon-right"></i></a> <a id="next_commit_link" data-commit-id="${c.next_commit.raw_id}" class=" ${('disabled' if c.url_next == '#' else '')}" href="${c.url_next}" title="${_('Next commit')}"><i class="icon-right"></i></a>

View file

@ -225,7 +225,9 @@
}, },
}); });
$(document).on("draw.dt", function () {
applyMiddleCut('.truncate-wrap .truncate a, .truncate-wrap'); // auto
});
}); });
</script> </script>
</%def> </%def>

View file

@ -315,8 +315,7 @@
prefix = '<i class="icon-tag"></i>'; prefix = '<i class="icon-tag"></i>';
} }
var originalOption = data.element; return prefix + escapeMarkup(truncateMiddle(data.text, 35));
return prefix + escapeMarkup(data.text);
}; };
// custom code mirror // custom code mirror
@ -423,6 +422,20 @@
reviewersController = new ReviewersController(); reviewersController = new ReviewersController();
reviewersController.diffDataHandler = diffDataHandler; reviewersController.diffDataHandler = diffDataHandler;
function truncateSelection(item) {
if (item.text !== undefined) {
return truncateMiddle(item.text, 35);
}
return item;
}
function truncateResult(item) {
if (item.text !== undefined) {
return truncateMiddle(item.text, 60);
}
return item;
}
var queryTargetRepo = function(self, query) { var queryTargetRepo = function(self, query) {
// cache ALL results if query is empty // cache ALL results if query is empty
var cacheKey = query.term || '__'; var cacheKey = query.term || '__';
@ -470,7 +483,9 @@
var globalDefaults = { var globalDefaults = {
dropdownAutoWidth: true, dropdownAutoWidth: true,
containerCssClass: "drop-menu", containerCssClass: "drop-menu",
dropdownCssClass: "drop-menu-dropdown" dropdownCssClass: "drop-menu-dropdown",
formatSelection: truncateSelection,
formatResult: truncateResult
}; };
var initSelect2 = function(defaultOptions) { var initSelect2 = function(defaultOptions) {

View file

@ -105,11 +105,11 @@
<div class="pr-commit-flow"> <div class="pr-commit-flow">
## Source ## Source
%if c.pull_request.source_ref_parts.type == 'branch': %if c.pull_request.source_ref_parts.type == 'branch':
<a href="${h.route_path('repo_commits', repo_name=c.pull_request.source_repo.repo_name, _query=dict(branch=c.pull_request.source_ref_parts.name))}"><code class="pr-source-info">${c.pull_request.source_ref_parts.type}:${c.pull_request.source_ref_parts.name}</code></a> <a href="${h.route_path('repo_commits', repo_name=c.pull_request.source_repo.repo_name, _query=dict(branch=c.pull_request.source_ref_parts.name))}"><code class="pr-source-info">${c.pull_request.source_ref_parts.type}:${h.truncate_middle(c.pull_request.source_ref_parts.name, 40)}</code></a>
%else: %else:
<code class="pr-source-info">${'{}:{}'.format(c.pull_request.source_ref_parts.type, c.pull_request.source_ref_parts.name)}</code> <code class="pr-source-info">${'{}:{}'.format(c.pull_request.source_ref_parts.type, h.truncate_middle(c.pull_request.source_ref_parts.name, 40))}</code>
%endif %endif
${_('of')} <a href="${h.route_path('repo_summary', repo_name=c.pull_request.source_repo.repo_name)}">${c.pull_request.source_repo.repo_name}</a> ${_('of')} <a href="${h.route_path('repo_summary', repo_name=c.pull_request.source_repo.repo_name)}">${h.truncate_middle(c.pull_request.source_repo.repo_name, 40)}</a>
&rarr; &rarr;
## Target ## Target
%if c.pull_request.target_ref_parts.type == 'branch': %if c.pull_request.target_ref_parts.type == 'branch':
@ -118,7 +118,7 @@
<code class="pr-target-info">${'{}:{}'.format(c.pull_request.target_ref_parts.type, c.pull_request.target_ref_parts.name)}</code> <code class="pr-target-info">${'{}:{}'.format(c.pull_request.target_ref_parts.type, c.pull_request.target_ref_parts.name)}</code>
%endif %endif
${_('of')} <a href="${h.route_path('repo_summary', repo_name=c.pull_request.target_repo.repo_name)}">${c.pull_request.target_repo.repo_name}</a> ${_('of')} <a href="${h.route_path('repo_summary', repo_name=c.pull_request.target_repo.repo_name)}">${h.truncate_middle(c.pull_request.target_repo.repo_name, 40)}</a>
<a class="source-details-action" href="#expand-source-details" onclick="return toggleElement(this, '.source-details')" data-toggle-on='<i class="icon-angle-down">more details</i>' data-toggle-off='<i class="icon-angle-up">less details</i>'> <a class="source-details-action" href="#expand-source-details" onclick="return toggleElement(this, '.source-details')" data-toggle-on='<i class="icon-angle-down">more details</i>' data-toggle-off='<i class="icon-angle-up">less details</i>'>
<i class="icon-angle-down">more details</i> <i class="icon-angle-down">more details</i>