pullrequest.mako
593 lines
| 20.5 KiB
| application/x-mako
|
MakoHtmlLexer
r1282 | <%inherit file="/base/base.mako"/> | |||
<%def name="title()"> | ||||
${c.repo_name} ${_('New pull request')} | ||||
</%def> | ||||
<%def name="breadcrumbs_links()"> | ||||
${_('New pull request')} | ||||
</%def> | ||||
<%def name="menu_bar_nav()"> | ||||
${self.menu_items(active='repositories')} | ||||
</%def> | ||||
<%def name="menu_bar_subnav()"> | ||||
${self.repo_menu(active='showpullrequest')} | ||||
</%def> | ||||
<%def name="main()"> | ||||
<div class="box"> | ||||
<div class="title"> | ||||
${self.repo_page_title(c.rhodecode_db_repo)} | ||||
${self.breadcrumbs()} | ||||
</div> | ||||
${h.secure_form(url('pullrequest', repo_name=c.repo_name), method='post', id='pull_request_form')} | ||||
<div class="box pr-summary"> | ||||
<div class="summary-details block-left"> | ||||
<div class="form"> | ||||
<!-- fields --> | ||||
<div class="fields" > | ||||
<div class="field"> | ||||
<div class="label"> | ||||
<label for="pullrequest_title">${_('Title')}:</label> | ||||
</div> | ||||
<div class="input"> | ||||
${h.text('pullrequest_title', c.default_title, class_="medium autogenerated-title")} | ||||
</div> | ||||
</div> | ||||
<div class="field"> | ||||
<div class="label label-textarea"> | ||||
<label for="pullrequest_desc">${_('Description')}:</label> | ||||
</div> | ||||
<div class="textarea text-area editor"> | ||||
${h.textarea('pullrequest_desc',size=30, )} | ||||
<span class="help-block"> | ||||
${_('Write a short description on this pull request')} | ||||
</span> | ||||
</div> | ||||
</div> | ||||
<div class="field"> | ||||
<div class="label label-textarea"> | ||||
<label for="pullrequest_desc">${_('Commit flow')}:</label> | ||||
</div> | ||||
## TODO: johbo: Abusing the "content" class here to get the | ||||
## desired effect. Should be replaced by a proper solution. | ||||
##ORG | ||||
<div class="content"> | ||||
<strong>${_('Origin repository')}:</strong> | ||||
${c.rhodecode_db_repo.description} | ||||
</div> | ||||
<div class="content"> | ||||
${h.hidden('source_repo')} | ||||
${h.hidden('source_ref')} | ||||
</div> | ||||
##OTHER, most Probably the PARENT OF THIS FORK | ||||
<div class="content"> | ||||
## filled with JS | ||||
<div id="target_repo_desc"></div> | ||||
</div> | ||||
<div class="content"> | ||||
${h.hidden('target_repo')} | ||||
${h.hidden('target_ref')} | ||||
<span id="target_ref_loading" style="display: none"> | ||||
${_('Loading refs...')} | ||||
</span> | ||||
</div> | ||||
</div> | ||||
<div class="field"> | ||||
<div class="label label-textarea"> | ||||
<label for="pullrequest_submit"></label> | ||||
</div> | ||||
<div class="input"> | ||||
<div class="pr-submit-button"> | ||||
${h.submit('save',_('Submit Pull Request'),class_="btn")} | ||||
</div> | ||||
<div id="pr_open_message"></div> | ||||
</div> | ||||
</div> | ||||
<div class="pr-spacing-container"></div> | ||||
</div> | ||||
</div> | ||||
</div> | ||||
<div> | ||||
<div class="reviewers-title block-right"> | ||||
<div class="pr-details-title"> | ||||
${_('Pull request reviewers')} | ||||
<span class="calculate-reviewers"> - ${_('loading...')}</span> | ||||
</div> | ||||
</div> | ||||
<div id="reviewers" class="block-right pr-details-content reviewers"> | ||||
## members goes here, filled via JS based on initial selection ! | ||||
<input type="hidden" name="__start__" value="review_members:sequence"> | ||||
<ul id="review_members" class="group_members"></ul> | ||||
<input type="hidden" name="__end__" value="review_members:sequence"> | ||||
<div id="add_reviewer_input" class='ac'> | ||||
<div class="reviewer_ac"> | ||||
${h.text('user', class_='ac-input', placeholder=_('Add reviewer'))} | ||||
<div id="reviewers_container"></div> | ||||
</div> | ||||
</div> | ||||
</div> | ||||
</div> | ||||
</div> | ||||
<div class="box"> | ||||
<div> | ||||
## overview pulled by ajax | ||||
<div id="pull_request_overview"></div> | ||||
</div> | ||||
</div> | ||||
${h.end_form()} | ||||
</div> | ||||
<script type="text/javascript"> | ||||
$(function(){ | ||||
var defaultSourceRepo = '${c.default_repo_data['source_repo_name']}'; | ||||
var defaultSourceRepoData = ${c.default_repo_data['source_refs_json']|n}; | ||||
var defaultTargetRepo = '${c.default_repo_data['target_repo_name']}'; | ||||
var defaultTargetRepoData = ${c.default_repo_data['target_refs_json']|n}; | ||||
var targetRepoName = '${c.repo_name}'; | ||||
var $pullRequestForm = $('#pull_request_form'); | ||||
var $sourceRepo = $('#source_repo', $pullRequestForm); | ||||
var $targetRepo = $('#target_repo', $pullRequestForm); | ||||
var $sourceRef = $('#source_ref', $pullRequestForm); | ||||
var $targetRef = $('#target_ref', $pullRequestForm); | ||||
var calculateContainerWidth = function() { | ||||
var maxWidth = 0; | ||||
var repoSelect2Containers = ['#source_repo', '#target_repo']; | ||||
$.each(repoSelect2Containers, function(idx, value) { | ||||
$(value).select2('container').width('auto'); | ||||
var curWidth = $(value).select2('container').width(); | ||||
if (maxWidth <= curWidth) { | ||||
maxWidth = curWidth; | ||||
} | ||||
$.each(repoSelect2Containers, function(idx, value) { | ||||
$(value).select2('container').width(maxWidth + 10); | ||||
}); | ||||
}); | ||||
}; | ||||
var initRefSelection = function(selectedRef) { | ||||
return function(element, callback) { | ||||
// translate our select2 id into a text, it's a mapping to show | ||||
// simple label when selecting by internal ID. | ||||
var id, refData; | ||||
if (selectedRef === undefined) { | ||||
id = element.val(); | ||||
refData = element.val().split(':'); | ||||
} else { | ||||
id = selectedRef; | ||||
refData = selectedRef.split(':'); | ||||
} | ||||
var text = refData[1]; | ||||
if (refData[0] === 'rev') { | ||||
text = text.substring(0, 12); | ||||
} | ||||
var data = {id: id, text: text}; | ||||
callback(data); | ||||
}; | ||||
}; | ||||
var formatRefSelection = function(item) { | ||||
var prefix = ''; | ||||
var refData = item.id.split(':'); | ||||
if (refData[0] === 'branch') { | ||||
prefix = '<i class="icon-branch"></i>'; | ||||
} | ||||
else if (refData[0] === 'book') { | ||||
prefix = '<i class="icon-bookmark"></i>'; | ||||
} | ||||
else if (refData[0] === 'tag') { | ||||
prefix = '<i class="icon-tag"></i>'; | ||||
} | ||||
var originalOption = item.element; | ||||
return prefix + item.text; | ||||
}; | ||||
// custom code mirror | ||||
var codeMirrorInstance = initPullRequestsCodeMirror('#pullrequest_desc'); | ||||
var queryTargetRepo = function(self, query) { | ||||
// cache ALL results if query is empty | ||||
var cacheKey = query.term || '__'; | ||||
var cachedData = self.cachedDataSource[cacheKey]; | ||||
if (cachedData) { | ||||
query.callback({results: cachedData.results}); | ||||
} else { | ||||
$.ajax({ | ||||
url: pyroutes.url('pullrequest_repo_destinations', {'repo_name': targetRepoName}), | ||||
data: {query: query.term}, | ||||
dataType: 'json', | ||||
type: 'GET', | ||||
success: function(data) { | ||||
self.cachedDataSource[cacheKey] = data; | ||||
query.callback({results: data.results}); | ||||
}, | ||||
error: function(data, textStatus, errorThrown) { | ||||
alert( | ||||
"Error while fetching entries.\nError code {0} ({1}).".format(data.status, data.statusText)); | ||||
} | ||||
}); | ||||
} | ||||
}; | ||||
var queryTargetRefs = function(initialData, query) { | ||||
var data = {results: []}; | ||||
// filter initialData | ||||
$.each(initialData, function() { | ||||
var section = this.text; | ||||
var children = []; | ||||
$.each(this.children, function() { | ||||
if (query.term.length === 0 || | ||||
this.text.toUpperCase().indexOf(query.term.toUpperCase()) >= 0 ) { | ||||
children.push({'id': this.id, 'text': this.text}) | ||||
} | ||||
}); | ||||
data.results.push({'text': section, 'children': children}) | ||||
}); | ||||
query.callback({results: data.results}); | ||||
}; | ||||
var prButtonLockChecks = { | ||||
'compare': false, | ||||
'reviewers': false | ||||
}; | ||||
var prButtonLock = function(lockEnabled, msg, scope) { | ||||
scope = scope || 'all'; | ||||
if (scope == 'all'){ | ||||
prButtonLockChecks['compare'] = !lockEnabled; | ||||
prButtonLockChecks['reviewers'] = !lockEnabled; | ||||
} else if (scope == 'compare') { | ||||
prButtonLockChecks['compare'] = !lockEnabled; | ||||
} else if (scope == 'reviewers'){ | ||||
prButtonLockChecks['reviewers'] = !lockEnabled; | ||||
} | ||||
var checksMeet = prButtonLockChecks.compare && prButtonLockChecks.reviewers; | ||||
if (lockEnabled) { | ||||
$('#save').attr('disabled', 'disabled'); | ||||
} | ||||
else if (checksMeet) { | ||||
$('#save').removeAttr('disabled'); | ||||
} | ||||
if (msg) { | ||||
$('#pr_open_message').html(msg); | ||||
} | ||||
}; | ||||
var loadRepoRefDiffPreview = function() { | ||||
var sourceRepo = $sourceRepo.eq(0).val(); | ||||
var sourceRef = $sourceRef.eq(0).val().split(':'); | ||||
var targetRepo = $targetRepo.eq(0).val(); | ||||
var targetRef = $targetRef.eq(0).val().split(':'); | ||||
var url_data = { | ||||
'repo_name': targetRepo, | ||||
'target_repo': sourceRepo, | ||||
'source_ref': targetRef[2], | ||||
'source_ref_type': 'rev', | ||||
'target_ref': sourceRef[2], | ||||
'target_ref_type': 'rev', | ||||
'merge': true, | ||||
'_': Date.now() // bypass browser caching | ||||
}; // gather the source/target ref and repo here | ||||
if (sourceRef.length !== 3 || targetRef.length !== 3) { | ||||
prButtonLock(true, "${_('Please select origin and destination')}"); | ||||
return; | ||||
} | ||||
var url = pyroutes.url('compare_url', url_data); | ||||
// lock PR button, so we cannot send PR before it's calculated | ||||
prButtonLock(true, "${_('Loading compare ...')}", 'compare'); | ||||
if (loadRepoRefDiffPreview._currentRequest) { | ||||
loadRepoRefDiffPreview._currentRequest.abort(); | ||||
} | ||||
loadRepoRefDiffPreview._currentRequest = $.get(url) | ||||
.error(function(data, textStatus, errorThrown) { | ||||
alert( | ||||
"Error while processing request.\nError code {0} ({1}).".format( | ||||
data.status, data.statusText)); | ||||
}) | ||||
.done(function(data) { | ||||
loadRepoRefDiffPreview._currentRequest = null; | ||||
$('#pull_request_overview').html(data); | ||||
var commitElements = $(data).find('tr[commit_id]'); | ||||
var prTitleAndDesc = getTitleAndDescription(sourceRef[1], | ||||
commitElements, 5); | ||||
var title = prTitleAndDesc[0]; | ||||
var proposedDescription = prTitleAndDesc[1]; | ||||
var useGeneratedTitle = ( | ||||
$('#pullrequest_title').hasClass('autogenerated-title') || | ||||
$('#pullrequest_title').val() === ""); | ||||
if (title && useGeneratedTitle) { | ||||
// use generated title if we haven't specified our own | ||||
$('#pullrequest_title').val(title); | ||||
$('#pullrequest_title').addClass('autogenerated-title'); | ||||
} | ||||
var useGeneratedDescription = ( | ||||
!codeMirrorInstance._userDefinedDesc || | ||||
codeMirrorInstance.getValue() === ""); | ||||
if (proposedDescription && useGeneratedDescription) { | ||||
// set proposed content, if we haven't defined our own, | ||||
// or we don't have description written | ||||
codeMirrorInstance._userDefinedDesc = false; // reset state | ||||
codeMirrorInstance.setValue(proposedDescription); | ||||
} | ||||
var msg = ''; | ||||
if (commitElements.length === 1) { | ||||
msg = "${ungettext('This pull request will consist of __COMMITS__ commit.', 'This pull request will consist of __COMMITS__ commits.', 1)}"; | ||||
} else { | ||||
msg = "${ungettext('This pull request will consist of __COMMITS__ commit.', 'This pull request will consist of __COMMITS__ commits.', 2)}"; | ||||
} | ||||
msg += ' <a id="pull_request_overview_url" href="{0}" target="_blank">${_('Show detailed compare.')}</a>'.format(url); | ||||
if (commitElements.length) { | ||||
var commitsLink = '<a href="#pull_request_overview"><strong>{0}</strong></a>'.format(commitElements.length); | ||||
prButtonLock(false, msg.replace('__COMMITS__', commitsLink), 'compare'); | ||||
} | ||||
else { | ||||
prButtonLock(true, "${_('There are no commits to merge.')}", 'compare'); | ||||
} | ||||
}); | ||||
}; | ||||
/** | ||||
Generate Title and Description for a PullRequest. | ||||
In case of 1 commits, the title and description is that one commit | ||||
in case of multiple commits, we iterate on them with max N number of commits, | ||||
and build description in a form | ||||
- commitN | ||||
- commitN+1 | ||||
... | ||||
Title is then constructed from branch names, or other references, | ||||
replacing '-' and '_' into spaces | ||||
* @param sourceRef | ||||
* @param elements | ||||
* @param limit | ||||
* @returns {*[]} | ||||
*/ | ||||
var getTitleAndDescription = function(sourceRef, elements, limit) { | ||||
var title = ''; | ||||
var desc = ''; | ||||
$.each($(elements).get().reverse().slice(0, limit), function(idx, value) { | ||||
var rawMessage = $(value).find('td.td-description .message').data('messageRaw'); | ||||
desc += '- ' + rawMessage.split('\n')[0].replace(/\n+$/, "") + '\n'; | ||||
}); | ||||
// only 1 commit, use commit message as title | ||||
if (elements.length == 1) { | ||||
title = $(elements[0]).find('td.td-description .message').data('messageRaw').split('\n')[0]; | ||||
} | ||||
else { | ||||
// use reference name | ||||
title = sourceRef.replace(/-/g, ' ').replace(/_/g, ' ').capitalizeFirstLetter(); | ||||
} | ||||
return [title, desc] | ||||
}; | ||||
var Select2Box = function(element, overrides) { | ||||
var globalDefaults = { | ||||
dropdownAutoWidth: true, | ||||
containerCssClass: "drop-menu", | ||||
dropdownCssClass: "drop-menu-dropdown" | ||||
}; | ||||
var initSelect2 = function(defaultOptions) { | ||||
var options = jQuery.extend(globalDefaults, defaultOptions, overrides); | ||||
element.select2(options); | ||||
}; | ||||
return { | ||||
initRef: function() { | ||||
var defaultOptions = { | ||||
minimumResultsForSearch: 5, | ||||
formatSelection: formatRefSelection | ||||
}; | ||||
initSelect2(defaultOptions); | ||||
}, | ||||
initRepo: function(defaultValue, readOnly) { | ||||
var defaultOptions = { | ||||
initSelection : function (element, callback) { | ||||
var data = {id: defaultValue, text: defaultValue}; | ||||
callback(data); | ||||
} | ||||
}; | ||||
initSelect2(defaultOptions); | ||||
element.select2('val', defaultSourceRepo); | ||||
if (readOnly === true) { | ||||
element.select2('readonly', true); | ||||
} | ||||
} | ||||
}; | ||||
}; | ||||
var initTargetRefs = function(refsData, selectedRef){ | ||||
Select2Box($targetRef, { | ||||
query: function(query) { | ||||
queryTargetRefs(refsData, query); | ||||
}, | ||||
initSelection : initRefSelection(selectedRef) | ||||
}).initRef(); | ||||
if (!(selectedRef === undefined)) { | ||||
$targetRef.select2('val', selectedRef); | ||||
} | ||||
}; | ||||
var targetRepoChanged = function(repoData) { | ||||
// generate new DESC of target repo displayed next to select | ||||
$('#target_repo_desc').html( | ||||
"<strong>${_('Destination repository')}</strong>: {0}".format(repoData['description']) | ||||
); | ||||
// generate dynamic select2 for refs. | ||||
initTargetRefs(repoData['refs']['select2_refs'], | ||||
repoData['refs']['selected_ref']); | ||||
}; | ||||
var sourceRefSelect2 = Select2Box( | ||||
$sourceRef, { | ||||
placeholder: "${_('Select commit reference')}", | ||||
query: function(query) { | ||||
var initialData = defaultSourceRepoData['refs']['select2_refs']; | ||||
queryTargetRefs(initialData, query) | ||||
}, | ||||
initSelection: initRefSelection() | ||||
} | ||||
); | ||||
var sourceRepoSelect2 = Select2Box($sourceRepo, { | ||||
query: function(query) {} | ||||
}); | ||||
var targetRepoSelect2 = Select2Box($targetRepo, { | ||||
cachedDataSource: {}, | ||||
query: $.debounce(250, function(query) { | ||||
queryTargetRepo(this, query); | ||||
}), | ||||
formatResult: formatResult | ||||
}); | ||||
sourceRefSelect2.initRef(); | ||||
sourceRepoSelect2.initRepo(defaultSourceRepo, true); | ||||
targetRepoSelect2.initRepo(defaultTargetRepo, false); | ||||
$sourceRef.on('change', function(e){ | ||||
loadRepoRefDiffPreview(); | ||||
loadDefaultReviewers(); | ||||
}); | ||||
$targetRef.on('change', function(e){ | ||||
loadRepoRefDiffPreview(); | ||||
loadDefaultReviewers(); | ||||
}); | ||||
$targetRepo.on('change', function(e){ | ||||
var repoName = $(this).val(); | ||||
calculateContainerWidth(); | ||||
$targetRef.select2('destroy'); | ||||
$('#target_ref_loading').show(); | ||||
$.ajax({ | ||||
url: pyroutes.url('pullrequest_repo_refs', | ||||
{'repo_name': targetRepoName, 'target_repo_name':repoName}), | ||||
data: {}, | ||||
dataType: 'json', | ||||
type: 'GET', | ||||
success: function(data) { | ||||
$('#target_ref_loading').hide(); | ||||
targetRepoChanged(data); | ||||
loadRepoRefDiffPreview(); | ||||
}, | ||||
error: function(data, textStatus, errorThrown) { | ||||
alert("Error while fetching entries.\nError code {0} ({1}).".format(data.status, data.statusText)); | ||||
} | ||||
}) | ||||
}); | ||||
var loadDefaultReviewers = function() { | ||||
if (loadDefaultReviewers._currentRequest) { | ||||
loadDefaultReviewers._currentRequest.abort(); | ||||
} | ||||
$('.calculate-reviewers').show(); | ||||
prButtonLock(true, null, 'reviewers'); | ||||
var url = pyroutes.url('repo_default_reviewers_data', {'repo_name': targetRepoName}); | ||||
var sourceRepo = $sourceRepo.eq(0).val(); | ||||
var sourceRef = $sourceRef.eq(0).val().split(':'); | ||||
var targetRepo = $targetRepo.eq(0).val(); | ||||
var targetRef = $targetRef.eq(0).val().split(':'); | ||||
url += '?source_repo=' + sourceRepo; | ||||
url += '&source_ref=' + sourceRef[2]; | ||||
url += '&target_repo=' + targetRepo; | ||||
url += '&target_ref=' + targetRef[2]; | ||||
loadDefaultReviewers._currentRequest = $.get(url) | ||||
.done(function(data) { | ||||
loadDefaultReviewers._currentRequest = null; | ||||
// reset && add the reviewer based on selected repo | ||||
$('#review_members').html(''); | ||||
for (var i = 0; i < data.reviewers.length; i++) { | ||||
var reviewer = data.reviewers[i]; | ||||
addReviewMember( | ||||
reviewer.user_id, reviewer.firstname, | ||||
reviewer.lastname, reviewer.username, | ||||
reviewer.gravatar_link, reviewer.reasons); | ||||
} | ||||
$('.calculate-reviewers').hide(); | ||||
prButtonLock(false, null, 'reviewers'); | ||||
}); | ||||
}; | ||||
prButtonLock(true, "${_('Please select origin and destination')}", 'all'); | ||||
// auto-load on init, the target refs select2 | ||||
calculateContainerWidth(); | ||||
targetRepoChanged(defaultTargetRepoData); | ||||
$('#pullrequest_title').on('keyup', function(e){ | ||||
$(this).removeClass('autogenerated-title'); | ||||
}); | ||||
% if c.default_source_ref: | ||||
// in case we have a pre-selected value, use it now | ||||
$sourceRef.select2('val', '${c.default_source_ref}'); | ||||
loadRepoRefDiffPreview(); | ||||
loadDefaultReviewers(); | ||||
% endif | ||||
ReviewerAutoComplete('user'); | ||||
}); | ||||
</script> | ||||
</%def> | ||||