Merge branch 'master' into markdown-tags
This commit is contained in:
commit
057c8c344b
15
CHANGELOG
15
CHANGELOG
|
@ -2,12 +2,14 @@ Please view this file on the master branch, on stable branches it's out of date.
|
|||
|
||||
v 7.10.0 (unreleased)
|
||||
- Allow HTML tags in Markdown input
|
||||
- Include missing events and fix save functionality in admin service template settings form (Stan Hu)
|
||||
- Fix "Import projects from" button to show the correct instructions (Stan Hu)
|
||||
- Fix dots in Wiki slugs causing errors (Stan Hu)
|
||||
- Fix OAuth2 issue importing a new project from GitHub and GitLab (Stan Hu)
|
||||
- Update poltergeist to version 1.6.0 to support PhantomJS 2.0 (Zeger-Jan van de Weg)
|
||||
- Fix cross references when usernames, milestones, or project names contain underscores (Stan Hu)
|
||||
- Disable reference creation for comments surrounded by code/preformatted blocks (Stan Hu)
|
||||
- Reduce Rack Attack false positives causing 403 errors during HTTP authentication (Stan Hu)
|
||||
- enable line wrapping per default and remove the checkbox to toggle it (Hannes Rosenögger)
|
||||
- extend the commit calendar to show the actual commits made on a date (Hannes Rosenögger)
|
||||
- Fix a link in the patch update guide
|
||||
|
@ -17,6 +19,7 @@ v 7.10.0 (unreleased)
|
|||
- Add changelog, license and contribution guide links to project sidebar.
|
||||
- Improve diff UI
|
||||
- Fix alignment of navbar toggle button (Cody Mize)
|
||||
- Fix checkbox rendering for nested task lists
|
||||
- Identical look of selectboxes in UI
|
||||
- Move "Import existing repository by URL" option to button.
|
||||
- Improve error message when save profile has error.
|
||||
|
@ -28,8 +31,17 @@ v 7.10.0 (unreleased)
|
|||
- Restrict permissions on backup files
|
||||
- Improve oauth accounts UI in profile page
|
||||
- Add ability to unlink connected accounts
|
||||
- Replace commits calendar with faster contribution calendar that includes issues and merge requests
|
||||
- Add inifinite scroll to user page activity
|
||||
- Don't show commit comment button when user is not signed in.
|
||||
- Don't include system notes in issue/MR comment count.
|
||||
- Don't mark merge request as updated when merge status relative to target branch changes.
|
||||
- Link note avatar to user.
|
||||
|
||||
v 7.9.0
|
||||
- Send EmailsOnPush email when branch or tag is created or deleted.
|
||||
|
||||
v 7.9.0 (unreleased)
|
||||
- Add HipChat integration documentation (Stan Hu)
|
||||
- Update documentation for object_kind field in Webhook push and tag push Webhooks (Stan Hu)
|
||||
- Fix broken email images (Hannes Rosenögger)
|
||||
|
@ -146,7 +158,6 @@ v 7.8.0
|
|||
- Add API endpoint to fetch all changes on a MergeRequest (Jeroen van Baarsen)
|
||||
- View note image attachments in new tab when clicked instead of downloading them
|
||||
- Improve sorting logic in UI and API. Explicitly define what sorting method is used by default
|
||||
- Allow more variations for commit messages closing issues (Julien Bianchi and Hannes Rosenögger)
|
||||
- Fix overflow at sidebar when have several items
|
||||
- Add notes for label changes in issue and merge requests
|
||||
- Show tags in commit view (Hannes Rosenögger)
|
||||
|
@ -168,7 +179,7 @@ v 7.8.0
|
|||
- Add a commit calendar to the user profile (Hannes Rosenögger)
|
||||
- Submit comment on command-enter
|
||||
- Notify all members of a group when that group is mentioned in a comment, for example: `@gitlab-org` or `@sales`.
|
||||
- Extend issue clossing pattern to include "Resolve", "Resolves", "Resolved", "Resolving" and "Close"
|
||||
- Extend issue clossing pattern to include "Resolve", "Resolves", "Resolved", "Resolving" and "Close" (Julien Bianchi and Hannes Rosenögger)
|
||||
- Fix long broadcast message cut-off on left sidebar (Visay Keo)
|
||||
- Add Project Avatars (Steven Thonus and Hannes Rosenögger)
|
||||
- Password reset token validity increased from 2 hours to 2 days since it is also send on account creation.
|
||||
|
|
|
@ -188,7 +188,7 @@ GEM
|
|||
dotenv (>= 0.7)
|
||||
thor (>= 0.13.6)
|
||||
formatador (0.2.4)
|
||||
gemnasium-gitlab-service (0.2.5)
|
||||
gemnasium-gitlab-service (0.2.4)
|
||||
rugged (~> 0.21)
|
||||
gemojione (2.0.0)
|
||||
json
|
||||
|
@ -516,7 +516,7 @@ GEM
|
|||
rubyntlm (0.5.0)
|
||||
rubypants (0.2.0)
|
||||
rugged (0.21.4)
|
||||
rugments (1.0.0.beta5)
|
||||
rugments (1.0.0.beta6)
|
||||
safe_yaml (0.9.7)
|
||||
sanitize (2.1.0)
|
||||
nokogiri (>= 1.4.4)
|
||||
|
|
|
@ -7,7 +7,7 @@ class @calendar
|
|||
constructor: (timestamps, starting_year, starting_month, calendar_activities_path) ->
|
||||
cal = new CalHeatMap()
|
||||
cal.init
|
||||
itemName: ["commit"]
|
||||
itemName: ["contribution"]
|
||||
data: timestamps
|
||||
start: new Date(starting_year, starting_month)
|
||||
domainLabelFormat: "%b"
|
||||
|
@ -27,7 +27,6 @@ class @calendar
|
|||
legendCellPadding: 3
|
||||
onClick: (date, count) ->
|
||||
formated_date = date.getFullYear() + "-" + (date.getMonth()+1) + "-" + date.getDate()
|
||||
$(".calendar_commit_activity").fadeOut 400
|
||||
$.ajax
|
||||
url: calendar_activities_path
|
||||
data:
|
||||
|
@ -36,6 +35,4 @@ class @calendar
|
|||
dataType: "html"
|
||||
success: (data) ->
|
||||
$(".user-calendar-activities").html data
|
||||
$(".calendar_commit_activity").find(".js-toggle-content").hide()
|
||||
$(".calendar_commit_activity").fadeIn 400
|
||||
|
||||
|
|
|
@ -97,6 +97,7 @@ class Dispatcher
|
|||
new ProjectFork()
|
||||
when 'users:show'
|
||||
new User()
|
||||
new Activities()
|
||||
|
||||
switch path.first()
|
||||
when 'admin'
|
||||
|
|
|
@ -113,8 +113,14 @@ class @MergeRequest
|
|||
allowed_states = ["failed", "canceled", "running", "pending", "success"]
|
||||
if state in allowed_states
|
||||
$('.ci_widget.ci-' + state).show()
|
||||
switch state
|
||||
when "failed", "canceled"
|
||||
@setMergeButtonClass('btn-danger')
|
||||
when "running", "pending"
|
||||
@setMergeButtonClass('btn-warning')
|
||||
else
|
||||
$('.ci_widget.ci-error').show()
|
||||
@setMergeButtonClass('btn-danger')
|
||||
|
||||
showCiCoverage: (coverage) ->
|
||||
cov_html = $('<span>')
|
||||
|
@ -144,6 +150,9 @@ class @MergeRequest
|
|||
this.$('.merge-in-progress').hide()
|
||||
this.$('.automerge_widget.already_cannot_be_merged').show()
|
||||
|
||||
setMergeButtonClass: (css_class) ->
|
||||
$('.accept_merge_request').removeClass("btn-create").addClass(css_class)
|
||||
|
||||
mergeInProgress: ->
|
||||
$.ajax
|
||||
type: 'GET'
|
||||
|
|
|
@ -25,7 +25,7 @@ class @ProjectUsersSelect
|
|||
|
||||
initSelection: (element, callback) ->
|
||||
id = $(element).val()
|
||||
if id isnt ""
|
||||
if id != "" && id != "-1"
|
||||
Api.user(id, callback)
|
||||
|
||||
|
||||
|
@ -44,10 +44,7 @@ class @ProjectUsersSelect
|
|||
else
|
||||
avatar = gon.default_avatar_url
|
||||
|
||||
if user.id == ''
|
||||
avatarMarkup = ''
|
||||
else
|
||||
avatarMarkup = "<div class='user-image'><img class='avatar s24' src='#{avatar}'></div>"
|
||||
avatarMarkup = "<div class='user-image'><img class='avatar s24' src='#{avatar}'></div>"
|
||||
|
||||
"<div class='user-result'>
|
||||
#{avatarMarkup}
|
||||
|
|
|
@ -1,21 +1,8 @@
|
|||
.user-calendar-activities {
|
||||
|
||||
.calendar_commit_activity {
|
||||
padding: 5px 0 0;
|
||||
}
|
||||
|
||||
.calendar_onclick_hr {
|
||||
padding: 0;
|
||||
margin: 10px 0;
|
||||
}
|
||||
|
||||
.calendar_commit_date {
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.calendar_activity_summary {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.str-truncated {
|
||||
max-width: 70%;
|
||||
|
@ -31,14 +18,6 @@
|
|||
background-color: #ddd;
|
||||
}
|
||||
}
|
||||
|
||||
.commit-row-message {
|
||||
color: #333;
|
||||
&:hover {
|
||||
color: #444;
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
}
|
||||
/**
|
||||
* This overwrites the default values of the cal-heatmap gem
|
||||
|
|
|
@ -15,6 +15,11 @@
|
|||
word-break: break-all;
|
||||
margin-right: 200px;
|
||||
display: block;
|
||||
|
||||
.file-mode {
|
||||
margin-left: 10px;
|
||||
color: #777;
|
||||
}
|
||||
}
|
||||
|
||||
.diff-btn-group {
|
||||
|
@ -34,11 +39,6 @@
|
|||
font-family: $monospace_font;
|
||||
font-size: smaller;
|
||||
}
|
||||
|
||||
.file-mode {
|
||||
font-family: $monospace_font;
|
||||
margin-left: 10px;
|
||||
}
|
||||
}
|
||||
.diff-content {
|
||||
overflow: auto;
|
||||
|
|
|
@ -137,30 +137,15 @@
|
|||
background-color: #F1FAF1;
|
||||
}
|
||||
|
||||
&.ci-pending {
|
||||
color: #548;
|
||||
border-color: #548;
|
||||
background-color: #F4F1FA;
|
||||
}
|
||||
|
||||
&.ci-pending,
|
||||
&.ci-running {
|
||||
color: $gl-warning;
|
||||
border-color: $gl-warning;
|
||||
background-color: #FAF5F1;
|
||||
}
|
||||
|
||||
&.ci-failed {
|
||||
color: $gl-danger;
|
||||
border-color: $gl-danger;
|
||||
background-color: #FAF1F1;
|
||||
}
|
||||
|
||||
&.ci-canceled {
|
||||
color: $gl-warning;
|
||||
border-color: $gl-danger;
|
||||
background-color: #FAF5F1;
|
||||
}
|
||||
|
||||
&.ci-failed,
|
||||
&.ci-canceled,
|
||||
&.ci-error {
|
||||
color: $gl-danger;
|
||||
border-color: $gl-danger;
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
/**
|
||||
* Modern GitLab UI theme
|
||||
* Blue GitLab UI theme
|
||||
*/
|
||||
.ui_blue {
|
||||
@include dark-theme(#BECDE9, #2980b9, #1970a9, #096099);
|
||||
|
|
|
@ -46,7 +46,9 @@ class Admin::ServicesController < Admin::ApplicationController
|
|||
:user_key, :device, :priority, :sound, :bamboo_url, :username, :password,
|
||||
:build_key, :server, :teamcity_url, :build_type,
|
||||
:description, :issues_url, :new_issue_url, :restrict_to_branch,
|
||||
:send_from_committer_email, :disable_diffs
|
||||
:send_from_committer_email, :disable_diffs,
|
||||
:push_events, :tag_push_events, :note_events, :issues_events,
|
||||
:merge_requests_events
|
||||
])
|
||||
end
|
||||
end
|
||||
|
|
|
@ -4,10 +4,7 @@ class UsersController < ApplicationController
|
|||
layout :determine_layout
|
||||
|
||||
def show
|
||||
@contributed_projects = Project.
|
||||
where(id: authorized_projects_ids & @user.contributed_projects_ids).
|
||||
in_group_namespace.
|
||||
includes(:namespace).
|
||||
@contributed_projects = contributed_projects.joined(@user).
|
||||
reject(&:forked?)
|
||||
|
||||
@projects = @user.personal_projects.
|
||||
|
@ -16,24 +13,26 @@ class UsersController < ApplicationController
|
|||
# Collect only groups common for both users
|
||||
@groups = @user.groups & GroupsFinder.new.execute(current_user)
|
||||
|
||||
# Get user activity feed for projects common for both users
|
||||
@events = @user.recent_events.
|
||||
where(project_id: authorized_projects_ids).
|
||||
with_associations.limit(30)
|
||||
|
||||
@title = @user.name
|
||||
@title_url = user_path(@user)
|
||||
|
||||
respond_to do |format|
|
||||
format.html
|
||||
format.atom { render layout: false }
|
||||
|
||||
format.atom do
|
||||
load_events
|
||||
render layout: false
|
||||
end
|
||||
|
||||
format.json do
|
||||
load_events
|
||||
pager_json("events/_events", @events.count)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def calendar
|
||||
projects = Project.where(id: authorized_projects_ids & @user.contributed_projects_ids)
|
||||
|
||||
calendar = Gitlab::CommitsCalendar.new(projects, @user)
|
||||
calendar = contributions_calendar
|
||||
@timestamps = calendar.timestamps
|
||||
@starting_year = calendar.starting_year
|
||||
@starting_month = calendar.starting_month
|
||||
|
@ -42,20 +41,13 @@ class UsersController < ApplicationController
|
|||
end
|
||||
|
||||
def calendar_activities
|
||||
projects = Project.where(id: authorized_projects_ids & @user.contributed_projects_ids)
|
||||
@calendar_date = Date.parse(params[:date]) rescue nil
|
||||
@events = []
|
||||
|
||||
date = Date.parse(params[:date]) rescue nil
|
||||
if date
|
||||
@calendar_activities = Gitlab::CommitsCalendar.get_commits_for_date(projects, @user, date)
|
||||
else
|
||||
@calendar_activities = {}
|
||||
if @calendar_date
|
||||
@events = contributions_calendar.events_by_date(@calendar_date)
|
||||
end
|
||||
|
||||
# get the total number of unique commits
|
||||
@commit_count = @calendar_activities.values.flatten.map(&:id).uniq.count
|
||||
|
||||
@calendar_date = date
|
||||
|
||||
render 'calendar_activities', layout: false
|
||||
end
|
||||
|
||||
|
@ -82,4 +74,24 @@ class UsersController < ApplicationController
|
|||
@authorized_projects_ids ||=
|
||||
ProjectsFinder.new.execute(current_user).pluck(:id)
|
||||
end
|
||||
|
||||
def contributed_projects
|
||||
@contributed_projects = Project.
|
||||
where(id: authorized_projects_ids & @user.contributed_projects_ids).
|
||||
includes(:namespace)
|
||||
end
|
||||
|
||||
def contributions_calendar
|
||||
@contributions_calendar ||= Gitlab::ContributionsCalendar.
|
||||
new(contributed_projects.reject(&:forked?), @user)
|
||||
end
|
||||
|
||||
def load_events
|
||||
# Get user activity feed for projects common for both users
|
||||
@events = @user.recent_events.
|
||||
where(project_id: authorized_projects_ids).
|
||||
with_associations
|
||||
|
||||
@events = @events.limit(20).offset(params[:offset] || 0)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -58,22 +58,11 @@ module IssuesHelper
|
|||
end
|
||||
|
||||
def bulk_update_milestone_options
|
||||
options_for_select(['None (backlog)']) +
|
||||
options_for_select([['None (backlog)', -1]]) +
|
||||
options_from_collection_for_select(project_active_milestones, 'id',
|
||||
'title', params[:milestone_id])
|
||||
end
|
||||
|
||||
def bulk_update_assignee_options(project = @project)
|
||||
options_for_select(['None (unassigned)']) +
|
||||
options_from_collection_for_select(project.team.members, 'id',
|
||||
'name', params[:assignee_id])
|
||||
end
|
||||
|
||||
def assignee_options(object, project = @project)
|
||||
options_from_collection_for_select(project.team.members.sort_by(&:name),
|
||||
'id', 'name', object.assignee_id)
|
||||
end
|
||||
|
||||
def milestone_options(object)
|
||||
options_from_collection_for_select(object.project.milestones.active,
|
||||
'id', 'title', object.milestone_id)
|
||||
|
|
|
@ -146,6 +146,10 @@ module ProjectsHelper
|
|||
nav_tabs << feature if project.send :"#{feature}_enabled"
|
||||
end
|
||||
|
||||
if project.issues_enabled || project.merge_requests_enabled
|
||||
nav_tabs << [:milestones, :labels]
|
||||
end
|
||||
|
||||
nav_tabs.flatten
|
||||
end
|
||||
|
||||
|
|
|
@ -23,9 +23,9 @@ module SearchHelper
|
|||
# Autocomplete results for various settings pages
|
||||
def default_autocomplete
|
||||
[
|
||||
{ label: "My Profile settings", url: profile_path },
|
||||
{ label: "My SSH Keys", url: profile_keys_path },
|
||||
{ label: "My Dashboard", url: root_path },
|
||||
{ label: "Profile settings", url: profile_path },
|
||||
{ label: "SSH Keys", url: profile_keys_path },
|
||||
{ label: "Dashboard", url: root_path },
|
||||
{ label: "Admin Section", url: admin_root_path },
|
||||
]
|
||||
end
|
||||
|
|
|
@ -16,31 +16,69 @@ module Emails
|
|||
subject: subject("Project was moved"))
|
||||
end
|
||||
|
||||
def repository_push_email(project_id, recipient, author_id, branch, compare, reverse_compare = false, send_from_committer_email = false, disable_diffs = false)
|
||||
def repository_push_email(project_id, recipient, author_id: nil,
|
||||
ref: nil,
|
||||
action: nil,
|
||||
compare: nil,
|
||||
reverse_compare: false,
|
||||
send_from_committer_email: false,
|
||||
disable_diffs: false)
|
||||
unless author_id && ref && action
|
||||
raise ArgumentError, "missing keywords: author_id, ref, action"
|
||||
end
|
||||
|
||||
@project = Project.find(project_id)
|
||||
@author = User.find(author_id)
|
||||
@reverse_compare = reverse_compare
|
||||
@compare = compare
|
||||
@commits = Commit.decorate(compare.commits)
|
||||
@diffs = compare.diffs
|
||||
@branch = Gitlab::Git.ref_name(branch)
|
||||
@ref_name = Gitlab::Git.ref_name(ref)
|
||||
@ref_type = Gitlab::Git.tag_ref?(ref) ? "tag" : "branch"
|
||||
@action = action
|
||||
@disable_diffs = disable_diffs
|
||||
|
||||
@subject = "[#{@project.path_with_namespace}][#{@branch}] "
|
||||
if @compare
|
||||
@commits = Commit.decorate(compare.commits)
|
||||
@diffs = compare.diffs
|
||||
end
|
||||
|
||||
if @commits.length > 1
|
||||
@target_url = namespace_project_compare_url(@project.namespace,
|
||||
@project,
|
||||
from: Commit.new(@compare.base),
|
||||
to: Commit.new(@compare.head))
|
||||
@subject << "Deleted " if @reverse_compare
|
||||
@subject << "#{@commits.length} commits: #{@commits.first.title}"
|
||||
@action_name =
|
||||
case action
|
||||
when :create
|
||||
"pushed new"
|
||||
when :delete
|
||||
"deleted"
|
||||
else
|
||||
"pushed to"
|
||||
end
|
||||
|
||||
@subject = "[#{@project.path_with_namespace}]"
|
||||
@subject << "[#{@ref_name}]" if action == :push
|
||||
@subject << " "
|
||||
|
||||
if action == :push
|
||||
if @commits.length > 1
|
||||
@target_url = namespace_project_compare_url(@project.namespace,
|
||||
@project,
|
||||
from: Commit.new(@compare.base),
|
||||
to: Commit.new(@compare.head))
|
||||
@subject << "Deleted " if @reverse_compare
|
||||
@subject << "#{@commits.length} commits: #{@commits.first.title}"
|
||||
else
|
||||
@target_url = namespace_project_commit_url(@project.namespace,
|
||||
@project, @commits.first)
|
||||
|
||||
@subject << "Deleted 1 commit: " if @reverse_compare
|
||||
@subject << @commits.first.title
|
||||
end
|
||||
else
|
||||
@target_url = namespace_project_commit_url(@project.namespace,
|
||||
@project, @commits.first)
|
||||
unless action == :delete
|
||||
@target_url = namespace_project_tree_url(@project.namespace,
|
||||
@project, @ref_name)
|
||||
end
|
||||
|
||||
@subject << "Deleted 1 commit: " if @reverse_compare
|
||||
@subject << @commits.first.title
|
||||
subject_action = @action_name.dup
|
||||
subject_action[0] = subject_action[0].capitalize
|
||||
@subject << "#{subject_action} #{@ref_type} #{@ref_name}"
|
||||
end
|
||||
|
||||
@disable_footer = true
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
# Used by MergeRequest and Issue
|
||||
module Taskable
|
||||
TASK_PATTERN_MD = /^(?<bullet> *[*-] *)\[(?<checked>[ xX])\]/.freeze
|
||||
TASK_PATTERN_HTML = /^<li>\[(?<checked>[ xX])\]/.freeze
|
||||
TASK_PATTERN_HTML = /^<li>(?<p_tag>\s*<p>)?\[(?<checked>[ xX])\]/.freeze
|
||||
|
||||
# Change the state of a task list item for this Taskable. Edit the object's
|
||||
# description by finding the nth task item and changing its checkbox
|
||||
|
|
|
@ -55,6 +55,12 @@ class Event < ActiveRecord::Base
|
|||
order('id DESC').limit(100).
|
||||
update_all(updated_at: Time.now)
|
||||
end
|
||||
|
||||
def contributions
|
||||
where("action = ? OR (target_type in (?) AND action in (?))",
|
||||
Event::PUSHED, ["MergeRequest", "Issue"],
|
||||
[Event::CREATED, Event::CLOSED, Event::MERGED])
|
||||
end
|
||||
end
|
||||
|
||||
def proper?
|
||||
|
|
|
@ -105,6 +105,15 @@ class MergeRequest < ActiveRecord::Base
|
|||
state :unchecked
|
||||
state :can_be_merged
|
||||
state :cannot_be_merged
|
||||
|
||||
around_transition do |merge_request, transition, block|
|
||||
merge_request.record_timestamps = false
|
||||
begin
|
||||
block.call
|
||||
ensure
|
||||
merge_request.record_timestamps = true
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
validates :source_project, presence: true, unless: :allow_broken
|
||||
|
|
|
@ -48,6 +48,7 @@ class Note < ActiveRecord::Base
|
|||
scope :inline, ->{ where("line_code IS NOT NULL") }
|
||||
scope :not_inline, ->{ where(line_code: [nil, '']) }
|
||||
scope :system, ->{ where(system: true) }
|
||||
scope :user, ->{ where(system: false) }
|
||||
scope :common, ->{ where(noteable_type: ["", nil]) }
|
||||
scope :fresh, ->{ order(created_at: :asc, id: :asc) }
|
||||
scope :inc_author_project, ->{ includes(:project, :author) }
|
||||
|
|
|
@ -1,32 +0,0 @@
|
|||
class ProjectContributions
|
||||
attr_reader :project, :user
|
||||
|
||||
def initialize(project, user)
|
||||
@project, @user = project, user
|
||||
end
|
||||
|
||||
def commits_log
|
||||
repository = project.repository
|
||||
|
||||
if !repository.exists? || repository.empty?
|
||||
return {}
|
||||
end
|
||||
|
||||
Rails.cache.fetch(cache_key) do
|
||||
repository.commits_per_day_for_user(user)
|
||||
end
|
||||
end
|
||||
|
||||
def user_commits_on_date(date)
|
||||
repository = @project.repository
|
||||
|
||||
if !repository.exists? || repository.empty?
|
||||
return []
|
||||
end
|
||||
commits = repository.commits_by_user_on_date_log(@user, date)
|
||||
end
|
||||
|
||||
def cache_key
|
||||
"#{Date.today.to_s}-commits-log-#{project.id}-#{user.email}"
|
||||
end
|
||||
end
|
|
@ -36,13 +36,19 @@ class EmailsOnPushService < Service
|
|||
end
|
||||
|
||||
def supported_events
|
||||
%w(push)
|
||||
%w(push tag_push)
|
||||
end
|
||||
|
||||
def execute(push_data)
|
||||
return unless supported_events.include?(push_data[:object_kind])
|
||||
|
||||
EmailsOnPushWorker.perform_async(project_id, recipients, push_data, send_from_committer_email?, disable_diffs?)
|
||||
EmailsOnPushWorker.perform_async(
|
||||
project_id,
|
||||
recipients,
|
||||
push_data,
|
||||
send_from_committer_email: send_from_committer_email?,
|
||||
disable_diffs: disable_diffs?
|
||||
)
|
||||
end
|
||||
|
||||
def send_from_committer_email?
|
||||
|
|
|
@ -149,41 +149,6 @@ class Repository
|
|||
end
|
||||
end
|
||||
|
||||
def timestamps_by_user_log(user)
|
||||
author_emails = '(' + user.all_emails.map{ |e| Regexp.escape(e) }.join('|') + ')'
|
||||
args = %W(git log -E --author=#{author_emails} --since=#{(Date.today - 1.year).to_s} --branches --pretty=format:%cd --date=short)
|
||||
dates = Gitlab::Popen.popen(args, path_to_repo).first.split("\n")
|
||||
|
||||
if dates.present?
|
||||
dates
|
||||
else
|
||||
[]
|
||||
end
|
||||
end
|
||||
|
||||
def commits_by_user_on_date_log(user, date)
|
||||
# format the date string for git
|
||||
start_date = date.strftime("%Y-%m-%d 00:00:00")
|
||||
end_date = date.strftime("%Y-%m-%d 23:59:59")
|
||||
|
||||
author_emails = '(' + user.all_emails.map{ |e| Regexp.escape(e) }.join('|') + ')'
|
||||
args = %W(git log -E --author=#{author_emails} --after=#{start_date.to_s} --until=#{end_date.to_s} --branches --pretty=format:%h)
|
||||
commits = Gitlab::Popen.popen(args, path_to_repo).first.split("\n")
|
||||
|
||||
commits.map! do |commit_id|
|
||||
commit(commit_id)
|
||||
end
|
||||
end
|
||||
|
||||
def commits_per_day_for_user(user)
|
||||
timestamps_by_user_log(user).
|
||||
group_by { |commit_date| commit_date }.
|
||||
inject({}) do |hash, (timestamp_date, commits)|
|
||||
hash[timestamp_date] = commits.count
|
||||
hash
|
||||
end
|
||||
end
|
||||
|
||||
def lookup_cache
|
||||
@lookup_cache ||= {}
|
||||
end
|
||||
|
|
|
@ -110,6 +110,7 @@ class User < ActiveRecord::Base
|
|||
has_many :notes, dependent: :destroy, foreign_key: :author_id
|
||||
has_many :merge_requests, dependent: :destroy, foreign_key: :author_id
|
||||
has_many :events, dependent: :destroy, foreign_key: :author_id, class_name: "Event"
|
||||
has_many :subscriptions, dependent: :destroy
|
||||
has_many :recent_events, -> { order "id DESC" }, foreign_key: :author_id, class_name: "Event"
|
||||
has_many :assigned_issues, dependent: :destroy, foreign_key: :assignee_id, class_name: "Issue"
|
||||
has_many :assigned_merge_requests, dependent: :destroy, foreign_key: :assignee_id, class_name: "MergeRequest"
|
||||
|
@ -603,13 +604,10 @@ class User < ActiveRecord::Base
|
|||
end
|
||||
|
||||
def contributed_projects_ids
|
||||
Event.where(author_id: self).
|
||||
Event.contributions.where(author_id: self).
|
||||
where("created_at > ?", Time.now - 1.year).
|
||||
where("action = :pushed OR (target_type = 'MergeRequest' AND action = :created)",
|
||||
pushed: Event::PUSHED, created: Event::CREATED).
|
||||
reorder(project_id: :desc).
|
||||
select(:project_id).
|
||||
uniq
|
||||
.map(&:project_id)
|
||||
uniq.map(&:project_id)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -4,9 +4,9 @@ module Issues
|
|||
issues_ids = params.delete(:issues_ids).split(",")
|
||||
issue_params = params
|
||||
|
||||
issue_params.delete(:state_event) unless issue_params[:state_event].present?
|
||||
issue_params.delete(:milestone_id) unless issue_params[:milestone_id].present?
|
||||
issue_params.delete(:assignee_id) unless issue_params[:assignee_id].present?
|
||||
issue_params.delete(:state_event) unless issue_params[:state_event].present?
|
||||
issue_params.delete(:milestone_id) unless issue_params[:milestone_id].present?
|
||||
issue_params.delete(:assignee_id) unless issue_params[:assignee_id].present?
|
||||
|
||||
issues = Issue.where(id: issues_ids)
|
||||
issues.each do |issue|
|
||||
|
|
|
@ -14,6 +14,9 @@ module Issues
|
|||
issue.update_nth_task(params[:task_num].to_i, false)
|
||||
end
|
||||
|
||||
params[:assignee_id] = "" if params[:assignee_id] == "-1"
|
||||
params[:milestone_id] = "" if params[:milestone_id] == "-1"
|
||||
|
||||
old_labels = issue.labels.to_a
|
||||
|
||||
if params.present? && issue.update_attributes(params.except(:state_event,
|
||||
|
|
|
@ -53,7 +53,7 @@ module MergeRequests
|
|||
|
||||
if merge_request.source_branch == @branch_name || force_push?
|
||||
merge_request.reload_code
|
||||
update_merge_request(merge_request)
|
||||
merge_request.mark_as_unchecked
|
||||
else
|
||||
mr_commit_ids = merge_request.commits.map(&:id)
|
||||
push_commit_ids = @commits.map(&:id)
|
||||
|
@ -61,20 +61,14 @@ module MergeRequests
|
|||
|
||||
if matches.any?
|
||||
merge_request.reload_code
|
||||
update_merge_request(merge_request)
|
||||
merge_request.mark_as_unchecked
|
||||
else
|
||||
update_merge_request(merge_request)
|
||||
merge_request.mark_as_unchecked
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def update_merge_request(merge_request)
|
||||
MergeRequests::UpdateService.new(
|
||||
merge_request.target_project,
|
||||
@current_user, merge_status: 'unchecked').execute(merge_request)
|
||||
end
|
||||
|
||||
# Add comment about pushing new commits to merge requests
|
||||
def comment_mr_with_commits
|
||||
merge_requests = @project.origin_merge_requests.opened.where(source_branch: @branch_name).to_a
|
||||
|
|
|
@ -23,6 +23,9 @@ module MergeRequests
|
|||
merge_request.update_nth_task(params[:task_num].to_i, false)
|
||||
end
|
||||
|
||||
params[:assignee_id] = "" if params[:assignee_id] == "-1"
|
||||
params[:milestone_id] = "" if params[:milestone_id] == "-1"
|
||||
|
||||
old_labels = merge_request.labels.to_a
|
||||
|
||||
if params.present? && merge_request.update_attributes(
|
||||
|
|
|
@ -21,13 +21,11 @@
|
|||
.form-group.js-toggle-colors-container.hide
|
||||
= f.label :color, "Background Color", class: 'control-label'
|
||||
.col-sm-10
|
||||
= f.text_field :color, placeholder: "#AA33EE", class: "form-control"
|
||||
.light 6 character hex values starting with a # sign.
|
||||
= f.color_field :color, value: "#AA33EE", class: "form-control"
|
||||
.form-group.js-toggle-colors-container.hide
|
||||
= f.label :font, "Font Color", class: 'control-label'
|
||||
.col-sm-10
|
||||
= f.text_field :font, placeholder: "#224466", class: "form-control"
|
||||
.light 6 character hex values starting with a # sign.
|
||||
= f.color_field :font, value: "#224466", class: "form-control"
|
||||
.form-group
|
||||
= f.label :starts_at, class: 'control-label'
|
||||
.col-sm-10.datetime-controls
|
||||
|
|
|
@ -14,6 +14,11 @@
|
|||
= preserve do
|
||||
= markdown @service.help
|
||||
|
||||
.form-group
|
||||
= f.label :active, "Active", class: "control-label"
|
||||
.col-sm-10
|
||||
= f.check_box :active
|
||||
|
||||
- if @service.supported_events.length > 1
|
||||
.form-group
|
||||
= f.label :url, "Trigger", class: 'control-label'
|
||||
|
@ -34,6 +39,14 @@
|
|||
%strong Tag push events
|
||||
%p.light
|
||||
This url will be triggered when a new tag is pushed to the repository
|
||||
- if @service.supported_events.include?("note")
|
||||
%div
|
||||
= f.check_box :note_events, class: 'pull-left'
|
||||
.prepend-left-20
|
||||
= f.label :note_events, class: 'list-label' do
|
||||
%strong Comments
|
||||
%p.light
|
||||
This url will be triggered when someone adds a comment
|
||||
- if @service.supported_events.include?("issue")
|
||||
%div
|
||||
= f.check_box :issues_events, class: 'pull-left'
|
||||
|
|
|
@ -6,4 +6,4 @@
|
|||
<p>You can confirm your account through the link below:</p>
|
||||
<% end %>
|
||||
|
||||
<p><%= link_to 'Confirm my account', confirmation_url(@resource, confirmation_token: @token) %></p>
|
||||
<p><%= link_to 'Confirm your account', confirmation_url(@resource, confirmation_token: @token) %></p>
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
<p>Someone has requested a link to change your password, and you can do this through the link below.</p>
|
||||
|
||||
<p><%= link_to 'Change my password', edit_password_url(@resource, reset_password_token: @token) %></p>
|
||||
<p><%= link_to 'Change your password', edit_password_url(@resource, reset_password_token: @token) %></p>
|
||||
|
||||
<p>If you didn't request this, please ignore this email.</p>
|
||||
<p>Your password won't change until you access the link above and create a new one.</p>
|
||||
|
|
|
@ -4,4 +4,4 @@
|
|||
|
||||
<p>Click the link below to unlock your account:</p>
|
||||
|
||||
<p><%= link_to 'Unlock my account', unlock_url(@resource, unlock_token: @token) %></p>
|
||||
<p><%= link_to 'Unlock your account', unlock_url(@resource, unlock_token: @token) %></p>
|
||||
|
|
|
@ -11,7 +11,7 @@
|
|||
%div
|
||||
= f.password_field :password_confirmation, class: "form-control bottom", placeholder: "Confirm new password", required: true
|
||||
.clearfix
|
||||
= f.submit "Change my password", class: "btn btn-primary"
|
||||
= f.submit "Change your password", class: "btn btn-primary"
|
||||
|
||||
.clearfix.prepend-top-20
|
||||
%p
|
||||
|
|
|
@ -21,8 +21,8 @@
|
|||
<div><%= f.submit "Update", class: "input_button" %></div>
|
||||
<% end %>
|
||||
|
||||
<h3>Cancel my account</h3>
|
||||
<h3>Cancel your account</h3>
|
||||
|
||||
<p>Unhappy? <%= link_to "Cancel my account", registration_path(resource_name), data: { confirm: "Are you sure?" }, method: :delete %>.</p>
|
||||
<p>Unhappy? <%= link_to "Cancel your account", registration_path(resource_name), data: { confirm: "Are you sure?" }, method: :delete %>.</p>
|
||||
|
||||
<%= link_to "Back", :back %>
|
||||
|
|
|
@ -53,7 +53,7 @@
|
|||
%code .panel .well-list
|
||||
|
||||
.panel.panel-default
|
||||
.panel-heading My list
|
||||
.panel-heading Your list
|
||||
%ul.well-list
|
||||
%li
|
||||
One item
|
||||
|
|
|
@ -25,7 +25,7 @@
|
|||
= link_to explore_root_path, title: "Explore", class: 'has_bottom_tooltip', 'data-original-title' => 'Public area' do
|
||||
%i.fa.fa-globe
|
||||
%li
|
||||
= link_to user_snippets_path(current_user), title: "My snippets", class: 'has_bottom_tooltip', 'data-original-title' => 'My snippets' do
|
||||
= link_to user_snippets_path(current_user), title: "Your snippets", class: 'has_bottom_tooltip', 'data-original-title' => 'Your snippets' do
|
||||
%i.fa.fa-clipboard
|
||||
- if current_user.is_admin?
|
||||
%li
|
||||
|
|
|
@ -44,11 +44,12 @@
|
|||
%span
|
||||
Graphs
|
||||
|
||||
= nav_link(controller: :milestones) do
|
||||
= link_to namespace_project_milestones_path(@project.namespace, @project), title: 'Milestones' do
|
||||
%i.fa.fa-clock-o
|
||||
%span
|
||||
Milestones
|
||||
- if project_nav_tab? :milestones
|
||||
= nav_link(controller: :milestones) do
|
||||
= link_to namespace_project_milestones_path(@project.namespace, @project), title: 'Milestones' do
|
||||
%i.fa.fa-clock-o
|
||||
%span
|
||||
Milestones
|
||||
|
||||
- if project_nav_tab? :issues
|
||||
= nav_link(controller: :issues) do
|
||||
|
@ -67,11 +68,12 @@
|
|||
Merge Requests
|
||||
%span.count.merge_counter= @project.merge_requests.opened.count
|
||||
|
||||
= nav_link(controller: :labels) do
|
||||
= link_to namespace_project_labels_path(@project.namespace, @project), title: 'Labels' do
|
||||
%i.fa.fa-tags
|
||||
%span
|
||||
Labels
|
||||
- if project_nav_tab? :labels
|
||||
= nav_link(controller: :labels) do
|
||||
= link_to namespace_project_labels_path(@project.namespace, @project), title: 'Labels' do
|
||||
%i.fa.fa-tags
|
||||
%span
|
||||
Labels
|
||||
|
||||
- if project_nav_tab? :wiki
|
||||
= nav_link(controller: :wikis) do
|
||||
|
|
|
@ -1,66 +1,67 @@
|
|||
%h3 #{@author.name} pushed to #{@branch} at #{link_to @project.name_with_namespace, namespace_project_url(@project.namespace, @project)}
|
||||
%h3 #{@author.name} #{@action_name} #{@ref_type} #{@ref_name} at #{link_to @project.name_with_namespace, namespace_project_url(@project.namespace, @project)}
|
||||
|
||||
- if @reverse_compare
|
||||
%p
|
||||
%strong WARNING:
|
||||
The push did not contain any new commits, but force pushed to delete the commits and changes below.
|
||||
- if @compare
|
||||
- if @reverse_compare
|
||||
%p
|
||||
%strong WARNING:
|
||||
The push did not contain any new commits, but force pushed to delete the commits and changes below.
|
||||
|
||||
%h4
|
||||
= @reverse_compare ? "Deleted commits:" : "Commits:"
|
||||
%h4
|
||||
= @reverse_compare ? "Deleted commits:" : "Commits:"
|
||||
|
||||
%ul
|
||||
- @commits.each do |commit|
|
||||
%li
|
||||
%strong #{link_to commit.short_id, namespace_project_commit_url(@project.namespace, @project, commit)}
|
||||
%div
|
||||
%span by #{commit.author_name}
|
||||
%i at #{commit.committed_date.strftime("%Y-%m-%dT%H:%M:%SZ")}
|
||||
%pre.commit-message
|
||||
= commit.safe_message
|
||||
%ul
|
||||
- @commits.each do |commit|
|
||||
%li
|
||||
%strong #{link_to commit.short_id, namespace_project_commit_url(@project.namespace, @project, commit)}
|
||||
%div
|
||||
%span by #{commit.author_name}
|
||||
%i at #{commit.committed_date.strftime("%Y-%m-%dT%H:%M:%SZ")}
|
||||
%pre.commit-message
|
||||
= commit.safe_message
|
||||
|
||||
%h4 #{pluralize @diffs.count, "changed file"}:
|
||||
%h4 #{pluralize @diffs.count, "changed file"}:
|
||||
|
||||
%ul
|
||||
- @diffs.each_with_index do |diff, i|
|
||||
%li.file-stats
|
||||
%a{href: "#{@target_url if @disable_diffs}#diff-#{i}" }
|
||||
- if diff.deleted_file
|
||||
%span.deleted-file
|
||||
−
|
||||
%ul
|
||||
- @diffs.each_with_index do |diff, i|
|
||||
%li.file-stats
|
||||
%a{href: "#{@target_url if @disable_diffs}#diff-#{i}" }
|
||||
- if diff.deleted_file
|
||||
%span.deleted-file
|
||||
−
|
||||
= diff.old_path
|
||||
- elsif diff.renamed_file
|
||||
= diff.old_path
|
||||
- elsif diff.renamed_file
|
||||
= diff.old_path
|
||||
→
|
||||
= diff.new_path
|
||||
- elsif diff.new_file
|
||||
%span.new-file
|
||||
+
|
||||
→
|
||||
= diff.new_path
|
||||
- elsif diff.new_file
|
||||
%span.new-file
|
||||
+
|
||||
= diff.new_path
|
||||
- else
|
||||
= diff.new_path
|
||||
- else
|
||||
= diff.new_path
|
||||
|
||||
- unless @disable_diffs
|
||||
%h4 Changes:
|
||||
- @diffs.each_with_index do |diff, i|
|
||||
%li{id: "diff-#{i}"}
|
||||
%a{href: @target_url + "#diff-#{i}"}
|
||||
- if diff.deleted_file
|
||||
%strong
|
||||
= diff.old_path
|
||||
deleted
|
||||
- elsif diff.renamed_file
|
||||
%strong
|
||||
= diff.old_path
|
||||
→
|
||||
%strong
|
||||
= diff.new_path
|
||||
- else
|
||||
%strong
|
||||
= diff.new_path
|
||||
%hr
|
||||
%pre
|
||||
= color_email_diff(diff.diff)
|
||||
%br
|
||||
- unless @disable_diffs
|
||||
%h4 Changes:
|
||||
- @diffs.each_with_index do |diff, i|
|
||||
%li{id: "diff-#{i}"}
|
||||
%a{href: @target_url + "#diff-#{i}"}
|
||||
- if diff.deleted_file
|
||||
%strong
|
||||
= diff.old_path
|
||||
deleted
|
||||
- elsif diff.renamed_file
|
||||
%strong
|
||||
= diff.old_path
|
||||
→
|
||||
%strong
|
||||
= diff.new_path
|
||||
- else
|
||||
%strong
|
||||
= diff.new_path
|
||||
%hr
|
||||
%pre
|
||||
= color_email_diff(diff.diff)
|
||||
%br
|
||||
|
||||
- if @compare.timeout
|
||||
%h5 Huge diff. To prevent performance issues changes are hidden
|
||||
- if @compare.timeout
|
||||
%h5 Huge diff. To prevent performance issues changes are hidden
|
||||
|
|
|
@ -1,47 +1,49 @@
|
|||
#{@author.name} pushed to #{@branch} at #{@project.name_with_namespace}
|
||||
\
|
||||
\
|
||||
- if @reverse_compare
|
||||
WARNING: The push did not contain any new commits, but force pushed to delete the commits and changes below.
|
||||
#{@author.name} #{@action_name} #{@ref_type} #{@ref_name} at #{@project.name_with_namespace}
|
||||
- if @compare
|
||||
\
|
||||
\
|
||||
= @reverse_compare ? "Deleted commits:" : "Commits:"
|
||||
- @commits.each do |commit|
|
||||
#{commit.short_id} by #{commit.author_name} at #{commit.committed_date.strftime("%Y-%m-%dT%H:%M:%SZ")}
|
||||
#{commit.safe_message}
|
||||
\- - - - -
|
||||
\
|
||||
\
|
||||
#{pluralize @diffs.count, "changed file"}:
|
||||
\
|
||||
- @diffs.each do |diff|
|
||||
- if diff.deleted_file
|
||||
\- − #{diff.old_path}
|
||||
- elsif diff.renamed_file
|
||||
\- #{diff.old_path} → #{diff.new_path}
|
||||
- elsif diff.new_file
|
||||
\- + #{diff.new_path}
|
||||
- else
|
||||
\- #{diff.new_path}
|
||||
- unless @disable_diffs
|
||||
\
|
||||
\
|
||||
Changes:
|
||||
- @diffs.each do |diff|
|
||||
- if @reverse_compare
|
||||
WARNING: The push did not contain any new commits, but force pushed to delete the commits and changes below.
|
||||
\
|
||||
\=====================================
|
||||
\
|
||||
= @reverse_compare ? "Deleted commits:" : "Commits:"
|
||||
- @commits.each do |commit|
|
||||
#{commit.short_id} by #{commit.author_name} at #{commit.committed_date.strftime("%Y-%m-%dT%H:%M:%SZ")}
|
||||
#{commit.safe_message}
|
||||
\- - - - -
|
||||
\
|
||||
\
|
||||
#{pluralize @diffs.count, "changed file"}:
|
||||
\
|
||||
- @diffs.each do |diff|
|
||||
- if diff.deleted_file
|
||||
#{diff.old_path} deleted
|
||||
\- − #{diff.old_path}
|
||||
- elsif diff.renamed_file
|
||||
#{diff.old_path} → #{diff.new_path}
|
||||
\- #{diff.old_path} → #{diff.new_path}
|
||||
- elsif diff.new_file
|
||||
\- + #{diff.new_path}
|
||||
- else
|
||||
= diff.new_path
|
||||
\=====================================
|
||||
!= diff.diff
|
||||
- if @compare.timeout
|
||||
\
|
||||
\
|
||||
Huge diff. To prevent performance issues it was hidden
|
||||
\
|
||||
\
|
||||
View it on GitLab: #{@target_url}
|
||||
\- #{diff.new_path}
|
||||
- unless @disable_diffs
|
||||
\
|
||||
\
|
||||
Changes:
|
||||
- @diffs.each do |diff|
|
||||
\
|
||||
\=====================================
|
||||
- if diff.deleted_file
|
||||
#{diff.old_path} deleted
|
||||
- elsif diff.renamed_file
|
||||
#{diff.old_path} → #{diff.new_path}
|
||||
- else
|
||||
= diff.new_path
|
||||
\=====================================
|
||||
!= diff.diff
|
||||
- if @compare.timeout
|
||||
\
|
||||
\
|
||||
Huge diff. To prevent performance issues it was hidden
|
||||
- if @target_url
|
||||
\
|
||||
\
|
||||
View it on GitLab: #{@target_url}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
%h3.page-title
|
||||
My Account History
|
||||
Your Account History
|
||||
%p.light
|
||||
All events created by your account are listed below.
|
||||
%hr
|
||||
|
|
|
@ -3,8 +3,6 @@
|
|||
.pull-right
|
||||
= link_to "Add SSH Key", new_profile_key_path, class: "btn btn-new"
|
||||
%p.light
|
||||
My SSH keys: #{@keys.count}
|
||||
%br
|
||||
Before you can add an SSH key you need to
|
||||
= link_to "generate it.", help_page_path("ssh", "README")
|
||||
%hr
|
||||
|
|
|
@ -19,7 +19,7 @@
|
|||
.fork-buttons
|
||||
- if current_user && can?(current_user, :fork_project, @project) && @project.namespace != current_user.namespace
|
||||
- if current_user.already_forked?(@project) && current_user.manageable_namespaces.size < 2
|
||||
= link_to namespace_project_path(current_user, current_user.fork_of(@project)), title: 'Go to my fork' do
|
||||
= link_to namespace_project_path(current_user, current_user.fork_of(@project)), title: 'Go to your fork' do
|
||||
= link_to_toggle_fork
|
||||
- else
|
||||
= link_to new_namespace_project_fork_path(@project.namespace, @project), title: "Fork project" do
|
||||
|
|
|
@ -13,7 +13,7 @@
|
|||
- note_count = @note_counts.fetch(commit.id, 0)
|
||||
- else
|
||||
- notes = project.notes.for_commit_id(commit.id)
|
||||
- note_count = notes.count
|
||||
- note_count = notes.user.count
|
||||
|
||||
- if note_count > 0
|
||||
%span.light
|
||||
|
|
|
@ -13,12 +13,13 @@
|
|||
- submodule_item = project.repository.blob_at(@commit.id, diff_file.file_path)
|
||||
= submodule_link(submodule_item, @commit.id)
|
||||
- else
|
||||
- if diff_file.renamed_file
|
||||
%span= "#{diff_file.old_path} renamed to #{diff_file.new_path}"
|
||||
- else
|
||||
%span= diff_file.new_path
|
||||
- if diff_file.mode_changed?
|
||||
%span.file-mode= "#{diff_file.diff.a_mode} → #{diff_file.diff.b_mode}"
|
||||
%span
|
||||
- if diff_file.renamed_file
|
||||
= "#{diff_file.old_path} renamed to #{diff_file.new_path}"
|
||||
- else
|
||||
= diff_file.new_path
|
||||
- if diff_file.mode_changed?
|
||||
%span.file-mode= "#{diff_file.diff.a_mode} → #{diff_file.diff.b_mode}"
|
||||
|
||||
.diff-btn-group
|
||||
- if blob.text?
|
||||
|
|
|
@ -16,7 +16,7 @@
|
|||
- else
|
||||
%td.old_line
|
||||
= link_to raw(type == "new" ? " " : line_old), "##{line_code}", id: line_code
|
||||
- if @comments_allowed
|
||||
- if @comments_allowed && can?(current_user, :write_note, @project)
|
||||
= link_to_new_diff_note(line_code)
|
||||
%td.new_line{data: {linenumber: line.new_pos}}
|
||||
= link_to raw(type == "old" ? " " : line.new_pos) , "##{line_code}", id: line_code
|
||||
|
|
|
@ -10,11 +10,12 @@
|
|||
- if issue.closed?
|
||||
%span
|
||||
CLOSED
|
||||
- if issue.notes.any?
|
||||
- note_count = issue.notes.user.count
|
||||
- if note_count > 0
|
||||
|
||||
%span
|
||||
%i.fa.fa-comments
|
||||
= issue.notes.count
|
||||
= note_count
|
||||
|
||||
.issue-info
|
||||
= link_to "##{issue.iid}", issue_path(issue), class: "light"
|
||||
|
|
|
@ -16,9 +16,9 @@
|
|||
.col-sm-10
|
||||
.input-group
|
||||
.input-group-addon.label-color-preview
|
||||
= f.color_field :color, placeholder: "#AA33EE", class: "form-control"
|
||||
= f.color_field :color, value: "#AA33EE", class: "form-control"
|
||||
.help-block
|
||||
6 character hex values starting with a # sign.
|
||||
Choose any color.
|
||||
%br
|
||||
Or you can choose one of suggested colors below
|
||||
|
||||
|
|
|
@ -16,11 +16,12 @@
|
|||
%span.label-branch<
|
||||
%i.fa.fa-code-fork
|
||||
%span= merge_request.target_branch
|
||||
- if merge_request.notes.any?
|
||||
- note_count = merge_request.mr_and_commit_notes.user.count
|
||||
- if note_count > 0
|
||||
|
||||
%span
|
||||
%i.fa.fa-comments
|
||||
= merge_request.mr_and_commit_notes.count
|
||||
= note_count
|
||||
.merge-request-info
|
||||
= link_to "##{merge_request.iid}", merge_request_path(merge_request), class: "light"
|
||||
- if merge_request.assignee
|
||||
|
|
|
@ -40,7 +40,7 @@
|
|||
= link_to merge_request_path(@merge_request) do
|
||||
%i.fa.fa-comments
|
||||
Discussion
|
||||
%span.badge= @merge_request.mr_and_commit_notes.count
|
||||
%span.badge= @merge_request.mr_and_commit_notes.user.count
|
||||
%li.commits-tab{data: {action: 'commits'}}
|
||||
= link_to merge_request_path(@merge_request), title: 'Commits' do
|
||||
%i.fa.fa-history
|
||||
|
|
|
@ -60,11 +60,12 @@
|
|||
Participants
|
||||
%span.badge= @users.count
|
||||
|
||||
.pull-right
|
||||
= link_to new_namespace_project_issue_path(@project.namespace, @project, issue: { milestone_id: @milestone.id }), class: "btn btn-grouped", title: "New Issue" do
|
||||
%i.fa.fa-plus
|
||||
New Issue
|
||||
= link_to 'Browse Issues', namespace_project_issues_path(@milestone.project.namespace, @milestone.project, milestone_id: @milestone.id), class: "btn edit-milestone-link btn-grouped"
|
||||
- if @project.issues_enabled
|
||||
.pull-right
|
||||
= link_to new_namespace_project_issue_path(@project.namespace, @project, issue: { milestone_id: @milestone.id }), class: "btn btn-grouped", title: "New Issue" do
|
||||
%i.fa.fa-plus
|
||||
New Issue
|
||||
= link_to 'Browse Issues', namespace_project_issues_path(@milestone.project.namespace, @milestone.project, milestone_id: @milestone.id), class: "btn edit-milestone-link btn-grouped"
|
||||
|
||||
.tab-content
|
||||
.tab-pane.active#tab-issues
|
||||
|
|
|
@ -2,7 +2,8 @@
|
|||
.timeline-entry
|
||||
.timeline-entry-inner
|
||||
.timeline-icon
|
||||
= image_tag avatar_icon(note.author_email), class: "avatar s40"
|
||||
= link_to user_path(note.author) do
|
||||
= image_tag avatar_icon(note.author_email), class: "avatar s40"
|
||||
.timeline-content
|
||||
- if note.for_merge_request?
|
||||
- if note.outdated?
|
||||
|
|
|
@ -4,7 +4,8 @@
|
|||
- if note.system
|
||||
%span.fa.fa-circle
|
||||
- else
|
||||
= image_tag avatar_icon(note.author_email), class: "avatar s40"
|
||||
= link_to user_path(note.author) do
|
||||
= image_tag avatar_icon(note.author_email), class: "avatar s40"
|
||||
.timeline-content
|
||||
.note-header
|
||||
.note-actions
|
||||
|
@ -21,7 +22,8 @@
|
|||
%i.fa.fa-trash-o.cred
|
||||
Remove
|
||||
- if note.system
|
||||
= image_tag avatar_icon(note.author_email), class: "avatar s16"
|
||||
= link_to user_path(note.author) do
|
||||
= image_tag avatar_icon(note.author_email), class: "avatar s16"
|
||||
= link_to_member(@project, note.author, avatar: false)
|
||||
%span.author-username
|
||||
= '@' + note.author.username
|
||||
|
|
|
@ -2,13 +2,13 @@
|
|||
- if diff
|
||||
.diff-file
|
||||
.diff-header
|
||||
- if diff.deleted_file
|
||||
%span= diff.old_path
|
||||
- else
|
||||
%span= diff.new_path
|
||||
- if diff.a_mode && diff.b_mode && diff.a_mode != diff.b_mode
|
||||
%span.file-mode= "#{diff.a_mode} → #{diff.b_mode}"
|
||||
%br/
|
||||
%span
|
||||
- if diff.deleted_file
|
||||
= diff.old_path
|
||||
- else
|
||||
= diff.new_path
|
||||
- if diff.a_mode && diff.b_mode && diff.a_mode != diff.b_mode
|
||||
%span.file-mode= "#{diff.a_mode} → #{diff.b_mode}"
|
||||
.diff-content
|
||||
%table
|
||||
- note.truncated_diff_lines.each do |line|
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
%h3.page-title
|
||||
My Snippets
|
||||
Your Snippets
|
||||
.pull-right
|
||||
= link_to new_snippet_path, class: "btn btn-new btn-grouped", title: "New Snippet" do
|
||||
Add new snippet
|
||||
|
|
|
@ -2,12 +2,12 @@
|
|||
Public snippets
|
||||
|
||||
.pull-right
|
||||
|
||||
|
||||
- if current_user
|
||||
= link_to new_snippet_path, class: "btn btn-new btn-grouped", title: "New Snippet" do
|
||||
Add new snippet
|
||||
= link_to user_snippets_path(current_user), class: "btn btn-grouped" do
|
||||
My snippets
|
||||
Your snippets
|
||||
|
||||
%p.light
|
||||
Public snippets created by you and other users are listed here
|
||||
|
|
|
@ -23,7 +23,7 @@
|
|||
.back-link
|
||||
- if @snippet.author == current_user
|
||||
= link_to user_snippets_path(current_user) do
|
||||
← my snippets
|
||||
← your snippets
|
||||
- else
|
||||
= link_to snippets_path do
|
||||
← discover snippets
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
- if @contributed_projects.present?
|
||||
.panel.panel-default
|
||||
.panel.panel-default.contributed-projects
|
||||
.panel-heading Projects contributed to
|
||||
= render 'shared/projects_list',
|
||||
projects: @contributed_projects.sort_by(&:star_count).reverse,
|
||||
|
|
|
@ -1,4 +1,7 @@
|
|||
%h4 Commits calendar
|
||||
%h4
|
||||
Contributions calendar
|
||||
.pull-right
|
||||
%small Issues, merge requests and push events
|
||||
#cal-heatmap.calendar
|
||||
:javascript
|
||||
new calendar(
|
||||
|
|
|
@ -1,33 +1,23 @@
|
|||
.calendar_commit_activity
|
||||
%hr
|
||||
%h4
|
||||
Commit Activity
|
||||
%strong
|
||||
- if @commit_count == 0
|
||||
no
|
||||
- else
|
||||
= @commit_count
|
||||
%span.calendar_commit_date
|
||||
unique
|
||||
= 'commit'.pluralize(@commit_count)
|
||||
on
|
||||
= @calendar_date.strftime("%b %d, %Y") rescue ''
|
||||
-unless @commit_count == 0
|
||||
%hr
|
||||
- @calendar_activities.each do |project, commits|
|
||||
- next if commits.empty?
|
||||
%div.js-toggle-container
|
||||
%h4.prepend-top-20
|
||||
%span.light Contributions for
|
||||
%strong #{@calendar_date.to_s(:short)}
|
||||
|
||||
%ul.bordered-list
|
||||
- @events.sort_by(&:created_at).each do |event|
|
||||
%li
|
||||
%span.light
|
||||
%i.fa.fa-clock-o
|
||||
= event.created_at.to_s(:time)
|
||||
- if event.push?
|
||||
#{event.action_name} #{event.ref_type} #{event.ref_name}
|
||||
- else
|
||||
= event_action_name(event)
|
||||
- if event.target
|
||||
%strong= link_to "##{event.target_iid}", [event.project.namespace.becomes(Namespace), event.project, event.target]
|
||||
|
||||
at
|
||||
%strong
|
||||
= pluralize(commits.count, 'commit')
|
||||
in project
|
||||
= link_to project.name_with_namespace, project_path(project)
|
||||
%a.text-expander.js-toggle-button …
|
||||
%hr
|
||||
%div.js-toggle-content
|
||||
- commits.each do |commit|
|
||||
%span.monospace
|
||||
= commit.committed_date.strftime("%H:%M")
|
||||
= link_to commit.short_id, namespace_project_commit_path(project.namespace, project, commit), class: "commit_short_id"
|
||||
= link_to commit.message, namespace_project_commit_path(project.namespace, project, commit), class: "commit-row-message str-truncated"
|
||||
%br
|
||||
%hr
|
||||
- if event.project
|
||||
= link_to_project event.project
|
||||
- else
|
||||
= event.project_name
|
||||
|
|
|
@ -40,7 +40,8 @@
|
|||
%strong
|
||||
%i.fa.fa-rss
|
||||
|
||||
= render @events
|
||||
.content_list
|
||||
= spinner
|
||||
%aside.col-md-4
|
||||
= render 'profile', user: @user
|
||||
= render 'projects'
|
||||
|
|
|
@ -1,40 +1,49 @@
|
|||
class EmailsOnPushWorker
|
||||
include Sidekiq::Worker
|
||||
|
||||
def perform(project_id, recipients, push_data, send_from_committer_email = false, disable_diffs = false)
|
||||
def perform(project_id, recipients, push_data, send_from_committer_email: false, disable_diffs: false)
|
||||
project = Project.find(project_id)
|
||||
before_sha = push_data["before"]
|
||||
after_sha = push_data["after"]
|
||||
branch = push_data["ref"]
|
||||
ref = push_data["ref"]
|
||||
author_id = push_data["user_id"]
|
||||
|
||||
if Gitlab::Git.blank_ref?(before_sha) || Gitlab::Git.blank_ref?(after_sha)
|
||||
# skip if new branch was pushed or branch was removed
|
||||
return true
|
||||
end
|
||||
action =
|
||||
if Gitlab::Git.blank_ref?(before_sha)
|
||||
:create
|
||||
elsif Gitlab::Git.blank_ref?(after_sha)
|
||||
:delete
|
||||
else
|
||||
:push
|
||||
end
|
||||
|
||||
compare = Gitlab::Git::Compare.new(project.repository.raw_repository, before_sha, after_sha)
|
||||
compare = nil
|
||||
reverse_compare = false
|
||||
if action == :push
|
||||
compare = Gitlab::Git::Compare.new(project.repository.raw_repository, before_sha, after_sha)
|
||||
|
||||
return false if compare.same
|
||||
return false if compare.same
|
||||
|
||||
if compare.commits.empty?
|
||||
compare = Gitlab::Git::Compare.new(project.repository.raw_repository, after_sha, before_sha)
|
||||
if compare.commits.empty?
|
||||
compare = Gitlab::Git::Compare.new(project.repository.raw_repository, after_sha, before_sha)
|
||||
|
||||
reverse_compare = true
|
||||
reverse_compare = true
|
||||
|
||||
return false if compare.commits.empty?
|
||||
return false if compare.commits.empty?
|
||||
end
|
||||
end
|
||||
|
||||
recipients.split(" ").each do |recipient|
|
||||
Notify.repository_push_email(
|
||||
project_id,
|
||||
recipient,
|
||||
author_id,
|
||||
branch,
|
||||
compare,
|
||||
reverse_compare,
|
||||
send_from_committer_email,
|
||||
disable_diffs
|
||||
author_id: author_id,
|
||||
ref: ref,
|
||||
action: action,
|
||||
compare: compare,
|
||||
reverse_compare: reverse_compare,
|
||||
send_from_committer_email: send_from_committer_email,
|
||||
disable_diffs: disable_diffs
|
||||
).deliver
|
||||
end
|
||||
ensure
|
||||
|
|
|
@ -285,6 +285,9 @@ production: &base
|
|||
|
||||
rack_attack:
|
||||
git_basic_auth:
|
||||
# Rack Attack IP banning enabled
|
||||
# enabled: true
|
||||
#
|
||||
# Whitelist requests from 127.0.0.1 for web proxies (NGINX/Apache) with incorrect headers
|
||||
# ip_whitelist: ["127.0.0.1"]
|
||||
#
|
||||
|
|
|
@ -183,6 +183,7 @@ Settings['extra'] ||= Settingslogic.new({})
|
|||
#
|
||||
Settings['rack_attack'] ||= Settingslogic.new({})
|
||||
Settings.rack_attack['git_basic_auth'] ||= Settingslogic.new({})
|
||||
Settings.rack_attack.git_basic_auth['enabled'] = true if Settings.rack_attack.git_basic_auth['enabled'].nil?
|
||||
Settings.rack_attack.git_basic_auth['ip_whitelist'] ||= %w{127.0.0.1}
|
||||
Settings.rack_attack.git_basic_auth['maxretry'] ||= 10
|
||||
Settings.rack_attack.git_basic_auth['findtime'] ||= 1.minute
|
||||
|
|
|
@ -0,0 +1,6 @@
|
|||
class SetIncorrectAssigneeIdToNull < ActiveRecord::Migration
|
||||
def up
|
||||
execute "UPDATE issues SET assignee_id = NULL WHERE assignee_id = -1"
|
||||
execute "UPDATE merge_requests SET assignee_id = NULL WHERE assignee_id = -1"
|
||||
end
|
||||
end
|
|
@ -11,7 +11,7 @@
|
|||
#
|
||||
# It's strongly recommended that you check this file into your version control system.
|
||||
|
||||
ActiveRecord::Schema.define(version: 20150320234437) do
|
||||
ActiveRecord::Schema.define(version: 20150324155957) do
|
||||
|
||||
# These are extensions that must be enabled in order to support this database
|
||||
enable_extension "plpgsql"
|
||||
|
|
|
@ -11,7 +11,7 @@ RUN apt-get update -q \
|
|||
# If the Omnibus package version below is outdated please contribute a merge request to update it.
|
||||
# If you run GitLab Enterprise Edition point it to a location where you have downloaded it.
|
||||
RUN TMP_FILE=$(mktemp); \
|
||||
wget -q -O $TMP_FILE https://downloads-packages.s3.amazonaws.com/ubuntu-14.04/gitlab_7.8.3-omnibus-1_amd64.deb \
|
||||
wget -q -O $TMP_FILE https://downloads-packages.s3.amazonaws.com/ubuntu-14.04/gitlab_7.9.0-omnibus.2-1_amd64.deb \
|
||||
&& dpkg -i $TMP_FILE \
|
||||
&& rm -f $TMP_FILE
|
||||
|
||||
|
|
|
@ -7,3 +7,10 @@ Feature: Admin Settings
|
|||
Scenario: Change application settings
|
||||
When I modify settings and save form
|
||||
Then I should see application settings saved
|
||||
|
||||
Scenario: Change Slack Service Template settings
|
||||
When I click on "Service Templates"
|
||||
And I click on "Slack" service
|
||||
Then I check all events and submit form
|
||||
And I should see service template settings saved
|
||||
And I should see all checkboxes checked
|
||||
|
|
|
@ -15,4 +15,33 @@ class Spinach::Features::AdminSettings < Spinach::FeatureSteps
|
|||
current_application_settings.home_page_url.should == 'https://about.gitlab.com/'
|
||||
page.should have_content 'Application settings saved successfully'
|
||||
end
|
||||
|
||||
step 'I click on "Service Templates"' do
|
||||
click_link 'Service Templates'
|
||||
end
|
||||
|
||||
step 'I click on "Slack" service' do
|
||||
click_link 'Slack'
|
||||
end
|
||||
|
||||
step 'I check all events and submit form' do
|
||||
page.check('Active')
|
||||
page.check('Push events')
|
||||
page.check('Tag push events')
|
||||
page.check('Comments')
|
||||
page.check('Issues events')
|
||||
page.check('Merge Request events')
|
||||
fill_in 'Webhook', with: "http://localhost"
|
||||
click_on 'Save'
|
||||
end
|
||||
|
||||
step 'I should see service template settings saved' do
|
||||
page.should have_content 'Application settings saved successfully'
|
||||
end
|
||||
|
||||
step 'I should see all checkboxes checked' do
|
||||
all('input[type=checkbox]').each do |checkbox|
|
||||
checkbox.should be_checked
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -7,4 +7,37 @@ class Spinach::Features::User < Spinach::FeatureSteps
|
|||
step 'I should see user "John Doe" page' do
|
||||
expect(title).to match(/^\s*John Doe/)
|
||||
end
|
||||
|
||||
step '"John Doe" has contributions' do
|
||||
user = User.find_by(name: 'John Doe')
|
||||
project = contributed_project
|
||||
|
||||
# Issue controbution
|
||||
issue_params = { title: 'Bug in old browser' }
|
||||
Issues::CreateService.new(project, user, issue_params).execute
|
||||
|
||||
# Push code contribution
|
||||
push_params = {
|
||||
project: project,
|
||||
action: Event::PUSHED,
|
||||
author_id: user.id,
|
||||
data: { commit_count: 3 }
|
||||
}
|
||||
|
||||
Event.create(push_params)
|
||||
end
|
||||
|
||||
step 'I should see contributed projects' do
|
||||
within '.contributed-projects' do
|
||||
page.should have_content(@contributed_project.name)
|
||||
end
|
||||
end
|
||||
|
||||
step 'I should see contributions calendar' do
|
||||
page.should have_css('.cal-heatmap-container')
|
||||
end
|
||||
|
||||
def contributed_project
|
||||
@contributed_project ||= create(:project, :public)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -67,3 +67,12 @@ Feature: User
|
|||
And I should see project "Enterprise"
|
||||
And I should not see project "Internal"
|
||||
And I should not see project "Community"
|
||||
|
||||
@javascript
|
||||
Scenario: "John Doe" contribution profile
|
||||
Given I sign in as a user
|
||||
And "John Doe" has contributions
|
||||
When I visit user "John Doe" page
|
||||
Then I should see user "John Doe" page
|
||||
And I should see contributed projects
|
||||
And I should see contributions calendar
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
require 'mime/types'
|
||||
require 'uri'
|
||||
|
||||
module API
|
||||
# Projects API
|
||||
|
@ -103,7 +104,7 @@ module API
|
|||
delete ":id/repository/branches/:branch" do
|
||||
authorize_push_project
|
||||
result = DeleteBranchService.new(user_project, current_user).
|
||||
execute(params[:branch])
|
||||
execute(URI.unescape(params[:branch]))
|
||||
|
||||
if result[:status] == :success
|
||||
{
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
require_relative 'rack_attack_helpers'
|
||||
require_relative 'shell_env'
|
||||
|
||||
module Grack
|
||||
|
@ -85,25 +86,41 @@ module Grack
|
|||
user = oauth_access_token_check(login, password)
|
||||
end
|
||||
|
||||
return user if user.present?
|
||||
|
||||
# At this point, we know the credentials were wrong. We let Rack::Attack
|
||||
# know there was a failed authentication attempt from this IP. This
|
||||
# information is stored in the Rails cache (Redis) and will be used by
|
||||
# the Rack::Attack middleware to decide whether to block requests from
|
||||
# this IP.
|
||||
# If the user authenticated successfully, we reset the auth failure count
|
||||
# from Rack::Attack for that IP. A client may attempt to authenticate
|
||||
# with a username and blank password first, and only after it receives
|
||||
# a 401 error does it present a password. Resetting the count prevents
|
||||
# false positives from occurring.
|
||||
#
|
||||
# Otherwise, we let Rack::Attack know there was a failed authentication
|
||||
# attempt from this IP. This information is stored in the Rails cache
|
||||
# (Redis) and will be used by the Rack::Attack middleware to decide
|
||||
# whether to block requests from this IP.
|
||||
config = Gitlab.config.rack_attack.git_basic_auth
|
||||
Rack::Attack::Allow2Ban.filter(@request.ip, config) do
|
||||
# Unless the IP is whitelisted, return true so that Allow2Ban
|
||||
# increments the counter (stored in Rails.cache) for the IP
|
||||
if config.ip_whitelist.include?(@request.ip)
|
||||
false
|
||||
|
||||
if config.enabled
|
||||
if user
|
||||
# A successful login will reset the auth failure count from this IP
|
||||
Rack::Attack::Allow2Ban.reset(@request.ip, config)
|
||||
else
|
||||
true
|
||||
banned = Rack::Attack::Allow2Ban.filter(@request.ip, config) do
|
||||
# Unless the IP is whitelisted, return true so that Allow2Ban
|
||||
# increments the counter (stored in Rails.cache) for the IP
|
||||
if config.ip_whitelist.include?(@request.ip)
|
||||
false
|
||||
else
|
||||
true
|
||||
end
|
||||
end
|
||||
|
||||
if banned
|
||||
Rails.logger.info "IP #{@request.ip} failed to login " \
|
||||
"as #{login} but has been temporarily banned from Git auth"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
nil # No user was found
|
||||
user
|
||||
end
|
||||
|
||||
def authorized_request?
|
||||
|
|
|
@ -0,0 +1,31 @@
|
|||
# rack-attack v4.2.0 doesn't yet support clearing of keys.
|
||||
# Taken from https://github.com/kickstarter/rack-attack/issues/113
|
||||
class Rack::Attack::Allow2Ban
|
||||
def self.reset(discriminator, options)
|
||||
findtime = options[:findtime] or raise ArgumentError, "Must pass findtime option"
|
||||
|
||||
cache.reset_count("#{key_prefix}:count:#{discriminator}", findtime)
|
||||
cache.delete("#{key_prefix}:ban:#{discriminator}")
|
||||
end
|
||||
end
|
||||
|
||||
class Rack::Attack::Cache
|
||||
def reset_count(unprefixed_key, period)
|
||||
epoch_time = Time.now.to_i
|
||||
# Add 1 to expires_in to avoid timing error: http://git.io/i1PHXA
|
||||
expires_in = period - (epoch_time % period) + 1
|
||||
key = "#{(epoch_time / period).to_i}:#{unprefixed_key}"
|
||||
delete(key)
|
||||
end
|
||||
|
||||
def delete(unprefixed_key)
|
||||
store.delete("#{prefix}:#{unprefixed_key}")
|
||||
end
|
||||
end
|
||||
|
||||
class Rack::Attack::StoreProxy::RedisStoreProxy
|
||||
def delete(key, options={})
|
||||
self.del(key)
|
||||
rescue Redis::BaseError
|
||||
end
|
||||
end
|
|
@ -1,41 +0,0 @@
|
|||
module Gitlab
|
||||
class CommitsCalendar
|
||||
attr_reader :timestamps
|
||||
|
||||
def initialize(projects, user)
|
||||
@timestamps = {}
|
||||
date_timestamps = []
|
||||
|
||||
projects.reject(&:forked?).each do |project|
|
||||
date_timestamps << ProjectContributions.new(project, user).commits_log
|
||||
end
|
||||
|
||||
# Sumarrize commits from all projects per days
|
||||
date_timestamps = date_timestamps.inject do |collection, date|
|
||||
collection.merge(date) { |k, old_v, new_v| old_v + new_v }
|
||||
end
|
||||
|
||||
date_timestamps ||= []
|
||||
date_timestamps.each do |date, commits|
|
||||
timestamp = Date.parse(date).to_time.to_i.to_s rescue nil
|
||||
@timestamps[timestamp] = commits if timestamp
|
||||
end
|
||||
end
|
||||
|
||||
def self.get_commits_for_date(projects, user, date)
|
||||
user_commits = {}
|
||||
projects.reject(&:forked?).each do |project|
|
||||
user_commits[project] = ProjectContributions.new(project, user).user_commits_on_date(date)
|
||||
end
|
||||
user_commits
|
||||
end
|
||||
|
||||
def starting_year
|
||||
(Time.now - 1.year).strftime("%Y")
|
||||
end
|
||||
|
||||
def starting_month
|
||||
Date.today.strftime("%m").to_i
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,56 @@
|
|||
module Gitlab
|
||||
class ContributionsCalendar
|
||||
attr_reader :timestamps, :projects, :user
|
||||
|
||||
def initialize(projects, user)
|
||||
@projects = projects
|
||||
@user = user
|
||||
end
|
||||
|
||||
def timestamps
|
||||
return @timestamps if @timestamps.present?
|
||||
|
||||
@timestamps = {}
|
||||
date_from = 1.year.ago
|
||||
date_to = Date.today
|
||||
|
||||
events = Event.reorder(nil).contributions.where(author_id: user.id).
|
||||
where("created_at > ?", date_from).where(project_id: projects).
|
||||
group('date(created_at)').
|
||||
select('date(created_at), count(id) as total_amount').
|
||||
map(&:attributes)
|
||||
|
||||
dates = (1.year.ago.to_date..(Date.today + 1.day)).to_a
|
||||
|
||||
dates.each do |date|
|
||||
date_id = date.to_time.to_i.to_s
|
||||
@timestamps[date_id] = 0
|
||||
day_events = events.find { |day_events| day_events["date"] == date }
|
||||
|
||||
if day_events
|
||||
@timestamps[date_id] = day_events["total_amount"]
|
||||
end
|
||||
end
|
||||
|
||||
@timestamps
|
||||
end
|
||||
|
||||
def events_by_date(date)
|
||||
events = Event.contributions.where(author_id: user.id).
|
||||
where("created_at > ? AND created_at < ?", date.beginning_of_day, date.end_of_day).
|
||||
where(project_id: projects)
|
||||
|
||||
events.select do |event|
|
||||
event.push? || event.issue? || event.merge_request?
|
||||
end
|
||||
end
|
||||
|
||||
def starting_year
|
||||
(Time.now - 1.year).strftime("%Y")
|
||||
end
|
||||
|
||||
def starting_month
|
||||
Date.today.strftime("%m").to_i
|
||||
end
|
||||
end
|
||||
end
|
|
@ -14,7 +14,6 @@ module Gitlab
|
|||
end
|
||||
|
||||
def self.find_by_dn(dn, adapter)
|
||||
dn = Net::LDAP::Filter.escape(dn)
|
||||
adapter.user('dn', dn)
|
||||
end
|
||||
|
||||
|
|
|
@ -368,11 +368,12 @@ module Gitlab
|
|||
# ActiveSupport::SafeBuffer, hence the `String.new`
|
||||
String.new(text).gsub(Taskable::TASK_PATTERN_HTML) do
|
||||
checked = $LAST_MATCH_INFO[:checked].downcase == 'x'
|
||||
p_tag = $LAST_MATCH_INFO[:p_tag]
|
||||
|
||||
if checked
|
||||
"#{li_tag}#{checked_box}"
|
||||
"#{li_tag}#{p_tag}#{checked_box}"
|
||||
else
|
||||
"#{li_tag}#{unchecked_box}"
|
||||
"#{li_tag}#{p_tag}#{unchecked_box}"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -67,7 +67,7 @@ module Gitlab
|
|||
end
|
||||
|
||||
def notes
|
||||
Note.where(project_id: limit_project_ids).search(query).order('updated_at DESC')
|
||||
Note.where(project_id: limit_project_ids).user.search(query).order('updated_at DESC')
|
||||
end
|
||||
|
||||
def limit_project_ids
|
||||
|
|
|
@ -25,34 +25,21 @@ describe UsersController do
|
|||
end
|
||||
|
||||
describe 'GET #calendar_activities' do
|
||||
include RepoHelpers
|
||||
let(:project) { create(:project) }
|
||||
let(:calendar_user) { create(:user, email: sample_commit.author_email) }
|
||||
let(:commit1) { '0ed8c6c6752e8c6ea63e7b92a517bf5ac1209c80' }
|
||||
let(:commit2) { '7d3b0f7cff5f37573aea97cebfd5692ea1689924' }
|
||||
let!(:project) { create(:project) }
|
||||
let!(:user) { create(:user) }
|
||||
|
||||
before do
|
||||
allow_any_instance_of(User).to receive(:contributed_projects_ids).and_return([project.id])
|
||||
project.team << [user, :developer]
|
||||
end
|
||||
|
||||
it 'assigns @commit_count' do
|
||||
get :calendar_activities, username: calendar_user.username, date: '2014-07-31'
|
||||
expect(assigns(:commit_count)).to eq(2)
|
||||
end
|
||||
|
||||
it 'assigns @calendar_date' do
|
||||
get :calendar_activities, username: calendar_user.username, date: '2014-07-31'
|
||||
get :calendar_activities, username: user.username, date: '2014-07-31'
|
||||
expect(assigns(:calendar_date)).to eq(Date.parse('2014-07-31'))
|
||||
end
|
||||
|
||||
it 'assigns @calendar_activities' do
|
||||
get :calendar_activities, username: calendar_user.username, date: '2014-07-31'
|
||||
expect(assigns(:calendar_activities).values.flatten.map(&:id)).to eq([commit1, commit2])
|
||||
end
|
||||
|
||||
it 'renders calendar_activities' do
|
||||
get :calendar_activities, username: calendar_user.username
|
||||
get :calendar_activities, username: user.username
|
||||
expect(response).to render_template('calendar_activities')
|
||||
end
|
||||
end
|
||||
|
|
|
@ -847,6 +847,17 @@ EOT
|
|||
)
|
||||
end
|
||||
|
||||
it 'should render checkboxes for nested tasks' do
|
||||
rendered_text = markdown(@source_text_asterisk, parse_tasks: true)
|
||||
|
||||
expect(rendered_text).to match(
|
||||
/<input.*checkbox.*valid unchecked nested task/
|
||||
)
|
||||
expect(rendered_text).to match(
|
||||
/<input.*checkbox.*valid checked nested task/
|
||||
)
|
||||
end
|
||||
|
||||
it 'should not be confused by whitespace before bullets' do
|
||||
rendered_text_asterisk = markdown(@source_text_asterisk,
|
||||
parse_tasks: true)
|
||||
|
|
|
@ -6,7 +6,7 @@ describe Grack::Auth do
|
|||
|
||||
let(:app) { lambda { |env| [200, {}, "Success!"] } }
|
||||
let!(:auth) { Grack::Auth.new(app) }
|
||||
let(:env) {
|
||||
let(:env) {
|
||||
{
|
||||
"rack.input" => "",
|
||||
"REQUEST_METHOD" => "GET",
|
||||
|
@ -85,6 +85,17 @@ describe Grack::Auth do
|
|||
it "responds with status 401" do
|
||||
expect(status).to eq(401)
|
||||
end
|
||||
|
||||
context "when the user is IP banned" do
|
||||
before do
|
||||
expect(Rack::Attack::Allow2Ban).to receive(:filter).and_return(true)
|
||||
allow_any_instance_of(Rack::Request).to receive(:ip).and_return('1.2.3.4')
|
||||
end
|
||||
|
||||
it "responds with status 401" do
|
||||
expect(status).to eq(401)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context "when authentication succeeds" do
|
||||
|
@ -109,10 +120,49 @@ describe Grack::Auth do
|
|||
end
|
||||
|
||||
context "when the user isn't blocked" do
|
||||
before do
|
||||
expect(Rack::Attack::Allow2Ban).to receive(:reset)
|
||||
end
|
||||
|
||||
it "responds with status 200" do
|
||||
expect(status).to eq(200)
|
||||
end
|
||||
end
|
||||
|
||||
context "when blank password attempts follow a valid login" do
|
||||
let(:options) { Gitlab.config.rack_attack.git_basic_auth }
|
||||
let(:maxretry) { options[:maxretry] - 1 }
|
||||
let(:ip) { '1.2.3.4' }
|
||||
|
||||
before do
|
||||
allow_any_instance_of(Rack::Request).to receive(:ip).and_return(ip)
|
||||
Rack::Attack::Allow2Ban.reset(ip, options)
|
||||
end
|
||||
|
||||
after do
|
||||
Rack::Attack::Allow2Ban.reset(ip, options)
|
||||
end
|
||||
|
||||
def attempt_login(include_password)
|
||||
password = include_password ? user.password : ""
|
||||
env["HTTP_AUTHORIZATION"] = ActionController::HttpAuthentication::Basic.encode_credentials(user.username, password)
|
||||
Grack::Auth.new(app)
|
||||
auth.call(env).first
|
||||
end
|
||||
|
||||
it "repeated attempts followed by successful attempt" do
|
||||
for n in 0..maxretry do
|
||||
expect(attempt_login(false)).to eq(401)
|
||||
end
|
||||
|
||||
expect(attempt_login(true)).to eq(200)
|
||||
expect(Rack::Attack::Allow2Ban.send(:banned?, ip)).to eq(nil)
|
||||
|
||||
for n in 0..maxretry do
|
||||
expect(attempt_login(false)).to eq(401)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context "when the user doesn't have access to the project" do
|
||||
|
|
|
@ -0,0 +1,35 @@
|
|||
require "spec_helper"
|
||||
|
||||
describe 'RackAttackHelpers' do
|
||||
describe 'reset' do
|
||||
let(:discriminator) { 'test-key'}
|
||||
let(:maxretry) { 5 }
|
||||
let(:period) { 1.minute }
|
||||
let(:options) { { findtime: period, bantime: 60, maxretry: maxretry } }
|
||||
|
||||
def do_filter
|
||||
for i in 1..maxretry - 1 do
|
||||
status = Rack::Attack::Allow2Ban.filter(discriminator, options) { true }
|
||||
expect(status).to eq(false)
|
||||
end
|
||||
end
|
||||
|
||||
def do_reset
|
||||
Rack::Attack::Allow2Ban.reset(discriminator, options)
|
||||
end
|
||||
|
||||
before do
|
||||
do_reset
|
||||
end
|
||||
|
||||
after do
|
||||
do_reset
|
||||
end
|
||||
|
||||
it 'user is not banned after n - 1 retries' do
|
||||
do_filter
|
||||
do_reset
|
||||
do_filter
|
||||
end
|
||||
end
|
||||
end
|
|
@ -640,6 +640,100 @@ describe Notify do
|
|||
end
|
||||
end
|
||||
|
||||
describe 'email on push for a created branch' do
|
||||
let(:example_site_path) { root_path }
|
||||
let(:user) { create(:user) }
|
||||
let(:tree_path) { namespace_project_tree_path(project.namespace, project, "master") }
|
||||
|
||||
subject { Notify.repository_push_email(project.id, 'devs@company.name', author_id: user.id, ref: 'refs/heads/master', action: :create) }
|
||||
|
||||
it 'is sent as the author' do
|
||||
sender = subject.header[:from].addrs[0]
|
||||
expect(sender.display_name).to eq(user.name)
|
||||
expect(sender.address).to eq(gitlab_sender)
|
||||
end
|
||||
|
||||
it 'is sent to recipient' do
|
||||
is_expected.to deliver_to 'devs@company.name'
|
||||
end
|
||||
|
||||
it 'has the correct subject' do
|
||||
is_expected.to have_subject /Pushed new branch master/
|
||||
end
|
||||
|
||||
it 'contains a link to the branch' do
|
||||
is_expected.to have_body_text /#{tree_path}/
|
||||
end
|
||||
end
|
||||
|
||||
describe 'email on push for a created tag' do
|
||||
let(:example_site_path) { root_path }
|
||||
let(:user) { create(:user) }
|
||||
let(:tree_path) { namespace_project_tree_path(project.namespace, project, "v1.0") }
|
||||
|
||||
subject { Notify.repository_push_email(project.id, 'devs@company.name', author_id: user.id, ref: 'refs/tags/v1.0', action: :create) }
|
||||
|
||||
it 'is sent as the author' do
|
||||
sender = subject.header[:from].addrs[0]
|
||||
expect(sender.display_name).to eq(user.name)
|
||||
expect(sender.address).to eq(gitlab_sender)
|
||||
end
|
||||
|
||||
it 'is sent to recipient' do
|
||||
is_expected.to deliver_to 'devs@company.name'
|
||||
end
|
||||
|
||||
it 'has the correct subject' do
|
||||
is_expected.to have_subject /Pushed new tag v1\.0/
|
||||
end
|
||||
|
||||
it 'contains a link to the tag' do
|
||||
is_expected.to have_body_text /#{tree_path}/
|
||||
end
|
||||
end
|
||||
|
||||
describe 'email on push for a deleted branch' do
|
||||
let(:example_site_path) { root_path }
|
||||
let(:user) { create(:user) }
|
||||
|
||||
subject { Notify.repository_push_email(project.id, 'devs@company.name', author_id: user.id, ref: 'refs/heads/master', action: :delete) }
|
||||
|
||||
it 'is sent as the author' do
|
||||
sender = subject.header[:from].addrs[0]
|
||||
expect(sender.display_name).to eq(user.name)
|
||||
expect(sender.address).to eq(gitlab_sender)
|
||||
end
|
||||
|
||||
it 'is sent to recipient' do
|
||||
is_expected.to deliver_to 'devs@company.name'
|
||||
end
|
||||
|
||||
it 'has the correct subject' do
|
||||
is_expected.to have_subject /Deleted branch master/
|
||||
end
|
||||
end
|
||||
|
||||
describe 'email on push for a deleted tag' do
|
||||
let(:example_site_path) { root_path }
|
||||
let(:user) { create(:user) }
|
||||
|
||||
subject { Notify.repository_push_email(project.id, 'devs@company.name', author_id: user.id, ref: 'refs/tags/v1.0', action: :delete) }
|
||||
|
||||
it 'is sent as the author' do
|
||||
sender = subject.header[:from].addrs[0]
|
||||
expect(sender.display_name).to eq(user.name)
|
||||
expect(sender.address).to eq(gitlab_sender)
|
||||
end
|
||||
|
||||
it 'is sent to recipient' do
|
||||
is_expected.to deliver_to 'devs@company.name'
|
||||
end
|
||||
|
||||
it 'has the correct subject' do
|
||||
is_expected.to have_subject /Deleted tag v1\.0/
|
||||
end
|
||||
end
|
||||
|
||||
describe 'email on push with multiple commits' do
|
||||
let(:example_site_path) { root_path }
|
||||
let(:user) { create(:user) }
|
||||
|
@ -648,7 +742,7 @@ describe Notify do
|
|||
let(:diff_path) { namespace_project_compare_path(project.namespace, project, from: Commit.new(compare.base), to: Commit.new(compare.head)) }
|
||||
let(:send_from_committer_email) { false }
|
||||
|
||||
subject { Notify.repository_push_email(project.id, 'devs@company.name', user.id, 'master', compare, false, send_from_committer_email) }
|
||||
subject { Notify.repository_push_email(project.id, 'devs@company.name', author_id: user.id, ref: 'refs/heads/master', action: :push, compare: compare, reverse_compare: false, send_from_committer_email: send_from_committer_email) }
|
||||
|
||||
it 'is sent as the author' do
|
||||
sender = subject.header[:from].addrs[0]
|
||||
|
@ -736,7 +830,7 @@ describe Notify do
|
|||
let(:commits) { Commit.decorate(compare.commits) }
|
||||
let(:diff_path) { namespace_project_commit_path(project.namespace, project, commits.first) }
|
||||
|
||||
subject { Notify.repository_push_email(project.id, 'devs@company.name', user.id, 'master', compare) }
|
||||
subject { Notify.repository_push_email(project.id, 'devs@company.name', author_id: user.id, ref: 'refs/heads/master', action: :push, compare: compare) }
|
||||
|
||||
it 'is sent as the author' do
|
||||
sender = subject.header[:from].addrs[0]
|
||||
|
|
|
@ -13,47 +13,16 @@ describe Repository do
|
|||
it { is_expected.not_to include('fix') }
|
||||
end
|
||||
|
||||
describe :tag_names_contains do
|
||||
subject { repository.tag_names_contains(sample_commit.id) }
|
||||
|
||||
it { is_expected.to include('v1.1.0') }
|
||||
it { is_expected.not_to include('v1.0.0') }
|
||||
end
|
||||
|
||||
describe :last_commit_for_path do
|
||||
subject { repository.last_commit_for_path(sample_commit.id, '.gitignore').id }
|
||||
|
||||
it { is_expected.to eq('c1acaa58bbcbc3eafe538cb8274ba387047b69f8') }
|
||||
end
|
||||
|
||||
context :timestamps_by_user_log do
|
||||
before do
|
||||
Date.stub(:today).and_return(Date.new(2015, 03, 01))
|
||||
end
|
||||
|
||||
describe 'single e-mail for user' do
|
||||
let(:user) { create(:user, email: sample_commit.author_email) }
|
||||
|
||||
subject { repository.timestamps_by_user_log(user) }
|
||||
|
||||
it { is_expected.to eq(['2014-08-06', '2014-07-31', '2014-07-31']) }
|
||||
end
|
||||
|
||||
describe 'multiple emails for user' do
|
||||
let(:email_alias) { create(:email, email: another_sample_commit.author_email) }
|
||||
let(:user) { create(:user, email: sample_commit.author_email, emails: [email_alias]) }
|
||||
|
||||
subject { repository.timestamps_by_user_log(user) }
|
||||
|
||||
it { is_expected.to eq(['2015-01-10', '2014-08-06', '2014-07-31', '2014-07-31']) }
|
||||
end
|
||||
end
|
||||
|
||||
context :commits_by_user_on_date_log do
|
||||
|
||||
describe 'single e-mail for user' do
|
||||
let(:user) { create(:user, email: sample_commit.author_email) }
|
||||
let(:commit1) { '0ed8c6c6752e8c6ea63e7b92a517bf5ac1209c80' }
|
||||
let(:commit2) { '7d3b0f7cff5f37573aea97cebfd5692ea1689924' }
|
||||
|
||||
subject { repository.commits_by_user_on_date_log(user,Date.new(2014, 07, 31)) }
|
||||
|
||||
it 'contains the exepected commits' do
|
||||
expect(subject.flatten.map(&:id)).to eq([commit1, commit2])
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
Loading…
Reference in New Issue