Commit 1f43fa20 authored by Dmitriy Zaporozhets's avatar Dmitriy Zaporozhets

Merge branch '6-4-stable' of dev.gitlab.org:gitlab/gitlabhq into 6-4-from-ce

Signed-off-by: default avatarDmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com>

Conflicts:
	Gemfile
	Gemfile.lock
	VERSION
	app/controllers/application_controller.rb
	db/schema.rb
	doc/install/installation.md
	doc/update/6.2-to-6.3.md
parents 20fc9711 0e4a8e23
......@@ -16,9 +16,12 @@ rvm:
- 2.0.0
services:
- mysql
- redis-server
before_script:
- "cp config/database.yml.$DB config/database.yml"
- "cp config/gitlab.yml.example config/gitlab.yml"
- "bundle exec rake db:setup"
- "bundle exec rake db:seed_fu"
script: "bundle exec rake $TASK --trace"
notifications:
email: false
v 6.4.0
- Added sorting to project issues page (Jason Blanchard)
- Assembla integration (Carlos Paramio)
- Fixed another 500 error with submodules
- UI: More compact issues page
- Minimal password length increased to 8 symbols
- Side-by-side diff view (Steven Thonus)
- Internal projects (Jason Hollingsworth)
- Allow removal of avatar (Drew Blessing)
- Project web hooks now support issues and merge request events
- Visiting project page while not logged in will redirect to sign-in instead of 404 (Jason Hollingsworth)
- Expire event cache on avatar creation/removal (Drew Blessing)
- Archiving old projects (Steven Thonus)
- Rails 4
- Add time ago tooltips to show actual date/time
- UI: Fixed UI for admin system hooks
- Ruby script for easier GitLab upgrade
- Do not remove Merge requests if fork project was removed
- Improve sign-in/signup UX
- Add resend confirmation link to sign-in page
- Set noreply@HOSTNAME for reply_to field in all emails
- Show GitLab API version on Admin#dashboard
- API Cross-origin resource sharing
- Show READMe link at project home page
- Show repo size for projects in Admin area
v 6.3.0
- API for adding gitlab-ci service
- Init script now waits for pids to appear after (re)starting before reporting status (Rovanion Luckey)
......@@ -58,7 +84,7 @@ v 6.2.0
- Avatar upload on profile page with a maximum of 100KB (Steven Thonus)
- Store the sessions in Redis instead of the cookie store
- Fixed relative links in markdown
- User must confirm his email if signup enabled
- User must confirm their email if signup enabled
- User must confirm changed email
v 6.1.0
......@@ -80,7 +106,7 @@ v 6.1.0
- Add links to create branch/tag from project home page
- Add public-project? checkbox to new-project view
- Improved compare page. Added link to proceed into Merge Request
- Send email to user when he was added to group
- Send an email to a user when they are added to group
- New landing page when you have 0 projects
v 6.0.0
......
......@@ -65,8 +65,13 @@ If you can, please submit a pull request with the fix or improvements including
1. Add your changes to the [CHANGELOG](CHANGELOG)
1. If you have multiple commits please combine them into one commit by [squashing them](http://git-scm.com/book/en/Git-Tools-Rewriting-History#Squashing-Commits)
1. Push the commit to your fork
1. Submit a pull request
2. [Search for issues](https://github.com/gitlabhq/gitlabhq/search?q=&ref=cmdform&type=Issues) related to your pull request and mention them in the pull request description
1. Submit a pull request (PR)
1. The PR title should describes the change you want to make
1. The PR description should give a motive for your change and the method you used to achieve it
* If the PR changes the UI it should include before and after screenshots
1. [Search for issues](https://github.com/gitlabhq/gitlabhq/search?q=&ref=cmdform&type=Issues) related to your pull request and mention them in the pull request description
Please keep the change in a single PR as small as possible. If you want to contribute a large feature think very hard what the minimum viable change is. Can you split functionality? Can you only submit the backend/API code? Can you start with a very simple UI? The smaller a PR is the more likely it is it will be merged, after that you can send more PR's to enhance it.
We will accept pull requests if:
......@@ -74,11 +79,9 @@ We will accept pull requests if:
* It can be merged without problems (if not please use: `git rebase master`)
* It does not break any existing functionality
* It's quality code that conforms to the [Ruby](https://github.com/bbatsov/ruby-style-guide) and [Rails](https://github.com/bbatsov/rails-style-guide) style guides and best practices
* The description includes a motive for your change and the method you used to achieve it
* It is not a catch all pull request but rather fixes a specific issue or implements a specific feature
* It keeps the GitLab code base clean and well structured
* We think other users will benefit from the same functionality
* If it makes changes to the UI the pull request should include screenshots
* It is a single commit (please use `git rebase -i` to squash commits)
For examples of feedback on pull requests please look at already [closed pull requests](https://github.com/gitlabhq/gitlabhq/pulls?direction=desc&page=1&sort=created&state=closed).
......@@ -8,15 +8,21 @@ def linux_only(require_as)
RUBY_PLATFORM.include?('linux') && require_as
end
gem "rails", "3.2.16"
gem "rails", "~> 4.0.0"
gem "protected_attributes"
gem 'rails-observers'
gem 'actionpack-page_caching'
gem 'actionpack-action_caching'
gem 'activerecord-deprecated_finders'
# Supported DBs
gem "mysql2", group: :mysql
gem "pg", group: :postgres
# Auth
gem "devise", '~> 2.2'
gem "devise-async"
gem "devise", '3.0.4'
gem "devise-async", '0.8.0'
gem 'omniauth', "~> 1.1.3"
gem 'omniauth-google-oauth2'
gem 'omniauth-twitter'
......@@ -24,27 +30,28 @@ gem 'omniauth-github'
# Extracting information from a git repository
# Provide access to Gitlab::Git library
gem "gitlab_git", "~> 3.0.0.rc2"
gem "gitlab_git", "~> 4.0.0.pre"
# Ruby/Rack Git Smart-HTTP Server Handler
gem 'gitlab-grack', '~> 1.0.1', require: 'grack'
gem 'gitlab-grack', '~> 2.0.0.pre', require: 'grack'
# LDAP Auth
gem 'gitlab_omniauth-ldap', '1.0.3', require: "omniauth-ldap"
gem 'net-ldap'
# Syntax highlighter
gem "gitlab-pygments.rb", '~> 0.3.2', require: 'pygments.rb'
gem "gitlab-pygments.rb", '~> 0.5.4', require: 'pygments.rb'
# Git Wiki
gem "gitlab-gollum-lib", "~> 1.0.1", require: 'gollum-lib'
gem "gitlab-gollum-lib", "~> 1.0.2", require: 'gollum-lib'
# Language detection
gem "github-linguist", require: "linguist"
gem "gitlab-linguist", "~> 2.9.6", require: "linguist"
# API
gem "grape", "~> 0.4.1"
gem "grape", "~> 0.6.1"
gem "grape-entity", "~> 0.3.0"
gem 'rack-cors', require: 'rack/cors'
# Format dates and times
# based on human-friendly examples
......@@ -79,7 +86,10 @@ gem "github-markup", "~> 0.7.4", require: 'github/markup'
gem "asciidoctor"
# Application server
gem "unicorn", '~> 4.6.3', group: :unicorn
group :unicorn do
gem "unicorn", '~> 4.6.3'
gem 'unicorn-worker-killer'
end
# State machine
gem "state_machine"
......@@ -128,26 +138,24 @@ gem "sanitize"
# Protect against bruteforcing
gem "rack-attack"
group :assets do
gem "sass-rails"
gem "coffee-rails"
gem "uglifier"
gem "therubyracer"
gem 'turbolinks'
gem 'jquery-turbolinks'
gem 'chosen-rails', "1.0.0"
gem 'select2-rails'
gem 'jquery-atwho-rails', "0.3.0"
gem "jquery-rails", "2.1.3"
gem "jquery-ui-rails", "2.0.2"
gem "modernizr", "2.6.2"
gem "raphael-rails", "~> 2.1.2"
gem 'bootstrap-sass'
gem "font-awesome-rails"
gem "gemoji", "~> 1.2.1", require: 'emoji/railtie'
gem "gon"
end
gem "sass-rails"
gem "coffee-rails"
gem "uglifier"
gem "therubyracer"
gem 'turbolinks'
gem 'jquery-turbolinks'
gem 'chosen-rails', "1.0.1"
gem 'select2-rails'
gem 'jquery-atwho-rails', "~> 0.3.3"
gem "jquery-rails", "2.1.3"
gem "jquery-ui-rails", "2.0.2"
gem "modernizr", "2.6.2"
gem "raphael-rails", "~> 2.1.2"
gem 'bootstrap-sass', '~> 2.3'
gem "font-awesome-rails", '~> 3.2'
gem "gemoji", "~> 1.3.0"
gem "gon", git: "https://github.com/gitlabhq/gon.git", ref: '58ca8e17273051cb370182cabd3602d1da6783ab'
group :development do
gem "annotate", "~> 2.6.0.beta2"
......@@ -170,7 +178,7 @@ end
group :development, :test do
gem 'coveralls', require: false
gem 'rails-dev-tweaks'
# gem 'rails-dev-tweaks'
gem 'spinach-rails'
gem "rspec-rails"
gem "capybara"
......@@ -199,7 +207,7 @@ group :development, :test do
gem 'poltergeist', '~> 1.4.1'
gem 'spork', '~> 1.0rc'
gem 'jasmine'
gem 'jasmine', '2.0.0.rc5'
end
group :test do
......
This diff is collapsed.
......@@ -32,7 +32,9 @@
* GitLab.com commercial services: [Homepage](http://www.gitlab.com/) | [Subscription](http://www.gitlab.com/subscription/) | [Consultancy](http://www.gitlab.com/consultancy/) | [GitLab Cloud](http://www.gitlab.com/cloud/) | [Blog](http://blog.gitlab.com/)
* GitLab CI: [Readme](https://github.com/gitlabhq/gitlab-ci/blob/master/README.md) of the GitLab open-source continuous integration server
* [GitLab Enterprise Edition](https://www.gitlab.com/features/) offers additional features that are useful for larger organizations (100+ users).
* [GitLab CI](https://github.com/gitlabhq/gitlab-ci/blob/master/README.md) is a continuous integration (CI) server that is easy to integrate with GitLab.
### Requirements
......@@ -46,30 +48,24 @@
### Installation
#### Official production installation
* [Installation guide for a production server](doc/install/installation.md)
#### Official installation methods
* [Manual installation guide for a production server](doc/install/installation.md)
#### Official development installation
* [GitLab Chef Cookbook](https://gitlab.com/gitlab-org/cookbook-gitlab/blob/master/README.md) This cookbook can be used both for development installations and production installations. If you want to [contribute](CONTRIBUTE.md) to GitLab we suggest you follow the [development installation on a virtual machine with Vagrant](https://gitlab.com/gitlab-org/cookbook-gitlab/blob/master/doc/development.md) instructions to install all testing dependencies.
If you want to contribute, please first read our [Contributing Guidelines](https://github.com/gitlabhq/gitlabhq/blob/master/CONTRIBUTING.md) and then we suggest you to use the Vagrant virtual machine project to get an environment working with all dependencies.
#### Third party one-click installers
* [Vagrant virtual machine for development](https://github.com/gitlabhq/gitlab-vagrant-vm)
* [Digital Ocean 1-Click Application Install](https://www.digitalocean.com/blog_posts/host-your-git-repositories-in-55-seconds-with-gitlab) Have a new server up in 55 seconds. Digital Ocean uses SSD disks which is great for an IO intensive app such as GitLab.
* [BitNami one-click installers](http://bitnami.com/stack/gitlab) This package contains both GitLab and GitLab CI. It is available as installer, virtual machine or for cloud hosting providers (Amazon Web Services/Azure/etc.).
#### Unofficial production installations
#### Unofficial installation methods
* [GitLab recipes](https://github.com/gitlabhq/gitlab-recipes) repository with unofficial guides for using GitLab with different software (operating systems, webservers, etc.) than the official version.
* [Installation guides](https://github.com/gitlabhq/gitlab-public-wiki/wiki/Unofficial-Installation-Guides) public wiki with unofficial guides to install GitLab on different operating systems.
* [Digital Ocean 1-Click Application Install](https://www.digitalocean.com/) Have a new server up in 55 seconds. Digital Ocean uses SSD disks which is great for an IO intensive app as GitLab. Look for GitLab under 'Select Image' => 'Applications' when creating a droplet.
* [BitNami one-click installers](http://bitnami.com/stack/gitlab) Get an image with GitLab and GitLab CI preinstalled for Amazon Web Services, Azure, VMware or your local server.
### New versions and upgrading
Since 2011 GitLab is released on the 22nd of every month. Every new release includes an upgrade guide.
......@@ -80,7 +76,6 @@ Since 2011 GitLab is released on the 22nd of every month. Every new release incl
* Features that will be in the next releases are listed on [the feedback and suggestions forum](http://feedback.gitlab.com/forums/176466-general) with the status [started](http://feedback.gitlab.com/forums/176466-general/status/796456) and [completed](http://feedback.gitlab.com/forums/176466-general/status/796457).
### Run in production mode
The Installation guide contains instructions on how to download an init script and run it automatically on boot. You can also start the init script manually:
......@@ -111,7 +106,7 @@ or start each component separately
* Run all tests
bundle exec rake gitlab:test
bundle exec rake gitlab:test RAILS_ENV=test
* [RSpec](http://rspec.info/) unit and functional tests
......@@ -148,15 +143,17 @@ or start each component separately
* [Mailing list](https://groups.google.com/forum/#!forum/gitlabhq) and [Stack Overflow](http://stackoverflow.com/questions/tagged/gitlab) are the best places to ask questions. For example you can use it if you have questions about: permission denied errors, invisible repos, can't clone/pull/push or with web hooks that don't fire. Please search for similar issues before posting your own, there's a good chance somebody else had the same issue you have now and has resolved it. There are a lot of helpful GitLab users there who may be able to help you quickly. If your particular issue turns out to be a bug, it will find its way from there to a fix.
* [Unofficial #gitlab IRC on Freenode](http://www.freenode.net/) is another way to get in touch with other GitLab users who may be able to help you.
* [Feedback and suggestions forum](http://feedback.gitlab.com) is the place to propose and discuss new features for GitLab.
* [Contributing guide](https://github.com/gitlabhq/gitlabhq/blob/master/CONTRIBUTING.md) describes how to submit pull requests and issues. Pull requests and issues not in line with the guidelines in this document will be closed.
* [Support subscription](http://www.gitlab.com/subscription/) connects you to the knowledge of GitLab experts that will resolve your issues and answer your questions.
* [Consultancy](http://www.gitlab.com/consultancy/) allows you hire GitLab experts for installations, upgrades and customizations.
* [Consultancy](http://www.gitlab.com/consultancy/) from the GitLab experts for installations, upgrades and customizations.
* [#gitlab IRC channel](http://www.freenode.net/) on Freenode to get in touch with other GitLab users and get help, it's managed by James Newton, Drew Blessing and Sam Gleske
* [Book](http://www.packtpub.com/gitlab-repository-management/book) written by GitLab enthusiast Jonathan M. Hethey is unofficial but it offers a good overview.
### Getting in touch
......
app/assets/images/favicon.ico

1.12 KB | W: | H:

app/assets/images/favicon.ico

32.2 KB | W: | H:

app/assets/images/favicon.ico
app/assets/images/favicon.ico
app/assets/images/favicon.ico
app/assets/images/favicon.ico
  • 2-up
  • Swipe
  • Onion skin
app/assets/images/logo-black.png

3.01 KB | W: | H:

app/assets/images/logo-black.png

2.95 KB | W: | H:

app/assets/images/logo-black.png
app/assets/images/logo-black.png
app/assets/images/logo-black.png
app/assets/images/logo-black.png
  • 2-up
  • Swipe
  • Onion skin
app/assets/images/logo-white.png

5.59 KB | W: | H:

app/assets/images/logo-white.png

8.14 KB | W: | H:

app/assets/images/logo-white.png
app/assets/images/logo-white.png
app/assets/images/logo-white.png
app/assets/images/logo-white.png
  • 2-up
  • Swipe
  • Onion skin
class BlobView
constructor: ->
# handle multi-line select
handleMultiSelect = (e) ->
[ first_line, last_line ] = parseSelectedLines()
[ line_number ] = parseSelectedLines($(this).attr("id"))
hash = "L#{line_number}"
if e.shiftKey and not isNaN(first_line) and not isNaN(line_number)
if line_number < first_line
last_line = first_line
first_line = line_number
else
last_line = line_number
hash = if first_line == last_line then "L#{first_line}" else "L#{first_line}-#{last_line}"
setHash(hash)
e.preventDefault()
# See if there are lines selected
# "#L12" and "#L34-56" supported
highlightBlobLines = ->
if window.location.hash isnt ""
matches = window.location.hash.match(/\#L(\d+)(\-(\d+))?/)
highlightBlobLines = (e) ->
[ first_line, last_line ] = parseSelectedLines()
unless isNaN first_line
$("#tree-content-holder .highlight .line").removeClass("hll")
$("#LC#{line}").addClass("hll") for line in [first_line..last_line]
$("#L#{first_line}").ScrollTo() unless e?
# parse selected lines from hash
# always return first and last line (initialized to NaN)
parseSelectedLines = (str) ->
first_line = NaN
last_line = NaN
hash = str || window.location.hash
if hash isnt ""
matches = hash.match(/\#?L(\d+)(\-(\d+))?/)
first_line = parseInt(matches?[1])
last_line = parseInt(matches?[3])
last_line = first_line if isNaN(last_line)
[ first_line, last_line ]
setHash = (hash) ->
hash = hash.replace(/^\#/, "")
nodes = $("#" + hash)
# if any nodes are using this id, they must be temporarily changed
# also, add a temporary div at the top of the screen to prevent scrolling
if nodes.length > 0
scroll_top = $(document).scrollTop()
nodes.attr("id", "")
tmp = $("<div></div>")
.css({ position: "absolute", visibility: "hidden", top: scroll_top + "px" })
.attr("id", hash)
.appendTo(document.body)
window.location.hash = hash
# restore the nodes
if nodes.length > 0
tmp.remove()
nodes.attr("id", hash)
unless isNaN first_line
last_line = first_line if isNaN(last_line)
$("#tree-content-holder .highlight .line").removeClass("hll")
$("#LC#{line}").addClass("hll") for line in [first_line..last_line]
$("#L#{first_line}").ScrollTo()
# initialize multi-line select
$("#tree-content-holder .line_numbers a[id^=L]").on("click", handleMultiSelect)
# Highlight the correct lines on load
highlightBlobLines()
# Highlight the correct lines when the hash part of the URL changes
$(window).on 'hashchange', highlightBlobLines
$(window).on("hashchange", highlightBlobLines)
@BlobView = BlobView
......@@ -4,13 +4,13 @@ class CommitsList
limit: 0
offset: 0
@disable = false
@showProgress: ->
$('.loading').show()
@hideProgress: ->
$('.loading').hide()
@init: (ref, limit) ->
$(".day-commits-table li.commit").live 'click', (event) ->
if event.target.nodeName != "A"
......@@ -21,7 +21,7 @@ class CommitsList
@data.ref = ref
@data.limit = limit
@data.offset = limit
this.initLoadMore()
this.showProgress()
......@@ -32,7 +32,9 @@ class CommitsList
url: location.href
data: @data
complete: this.hideProgress
dataType: "script"
success: (data) ->
CommitsList.append(data.count, data.html)
dataType: "json"
@append: (count, html) ->
$("#commits-list").append(html)
......@@ -40,7 +42,7 @@ class CommitsList
@data.offset += count
else
@disable = true
@initLoadMore: ->
$(document).unbind('scroll')
$(document).endlessScroll
......
......@@ -22,7 +22,7 @@
backgroundColor: '#DDD'
opacity: .4
)
reload: ->
Issues.initSelects()
Issues.initChecks()
......@@ -54,7 +54,16 @@
unless terms is last_terms
last_terms = terms
if terms.length >= 2 or terms.length is 0
form.submit()
$.ajax
type: "GET"
url: location.href
data: "issue_search=" + terms
complete: ->
$(".loading").hide()
success: (data) ->
$('.issues-holder').html(data.html)
Issues.reload()
dataType: "json"
checkChanged: ->
checked_issues = $(".selected_issue:checked")
......
window.updatePage = (data) ->
$.ajax({type: "GET", url: location.href, data: data, dataType: "script"})
window.slugify = (text) ->
text.replace(/[^-a-zA-Z0-9]+/g, '_').toLowerCase()
......@@ -56,7 +53,7 @@ window.unbindEvents = ->
document.addEventListener("page:fetch", startSpinner)
document.addEventListener("page:fetch", unbindEvents)
document.addEventListener("page:receive", stopSpinner)
document.addEventListener("page:change", stopSpinner)
$ ->
# Click a .one_click_select field, select the contents
......
......@@ -21,7 +21,7 @@ class MergeRequest
this.initMergeWidget()
this.$('.show-all-commits').on 'click', =>
this.showAllCommits()
modal = $('#modal_merge_info').modal(show: false)
# Local jQuery finder
......@@ -83,12 +83,12 @@ class MergeRequest
url: this.$('.nav-tabs .diffs-tab a').attr('href')
beforeSend: =>
this.$('.status').addClass 'loading'
complete: =>
@diffs_loaded = true
this.$('.status').removeClass 'loading'
dataType: 'script'
success: (data) =>
this.$(".diffs").html(data.html)
dataType: 'json'
showAllCommits: ->
this.$('.first-commits').remove()
......
......@@ -6,7 +6,7 @@ var NoteList = {
target_type: null,
init: function(tid, tt, path) {
NoteList.notes_path = path + ".js";
NoteList.notes_path = path + ".json";
NoteList.target_id = tid;
NoteList.target_type = tt;
NoteList.target_params = "target_type=" + NoteList.target_type + "&target_id=" + NoteList.target_id;
......@@ -411,7 +411,10 @@ var NoteList = {
data: NoteList.target_params,
complete: function(){ $('.js-notes-busy').removeClass("loading")},
beforeSend: function() { $('.js-notes-busy').addClass("loading") },
dataType: "script"
success: function(data) {
NoteList.setContent(data.html);
},
dataType: "json"
});
},
......@@ -419,7 +422,7 @@ var NoteList = {
* Called in response to getContent().
* Replaces the content of #notes-list with the given html.
*/
setContent: function(newNoteIds, html) {
setContent: function(html) {
$("#notes-list").html(html);
},
......
......@@ -19,8 +19,9 @@
data: "limit=" + @limit + "&offset=" + @offset
complete: ->
$(".loading").hide()
dataType: "script"
success: (data) ->
Pager.append(data.count, data.html)
dataType: "json"
append: (count, html) ->
$(".content_list").append html
......
......@@ -5,6 +5,7 @@ html {
/** LAYOUT **/
body {
-webkit-font-smoothing: antialiased;
margin-bottom: 20px;
}
......@@ -354,6 +355,7 @@ table {
.navbar-gitlab .navbar-inner .nav > li .btn-sign-in {
@extend .btn-new;
padding: 5px 15px;
text-shadow: none;
}
.broadcast-message {
......@@ -369,6 +371,10 @@ table {
&.input-large {
width: 210px;
}
&.input-clamp {
max-width: 100%;
}
}
.user-result {
......
......@@ -72,10 +72,11 @@
.ui-box-head {
.box-title {
font-size: 18px;
font-weight: normal;
font-size: 20px;
font-weight: 500;
line-height: 28px;
margin: 0;
color: #444;
}
h3 {
margin: 0;
......@@ -154,7 +155,7 @@
}
.row_title {
font-weight: bold;
font-weight: 500;
color: #444;
&:hover {
color: #444;
......
......@@ -6,6 +6,7 @@
.cblue { color: #29A }
.cblack { color: #111 }
.cdark { color: #444 }
.camber { color: #ffc000 }
.cwhite { color: #fff!important }
.bgred { background: #F2DEDE!important }
......@@ -127,3 +128,8 @@ pre.well-pre {
.dropdown-menu > li > a {
text-shadow: none;
}
.dropdown-menu > li > a:hover,
.dropdown-menu > li > a:focus {
background: #29b;
}
......@@ -13,6 +13,13 @@ form {
margin-top: 1px !important;
}
}
&.list-label {
float: none;
padding: 0 !important;
margin: 0;
text-align: left;
}
}
}
......
......@@ -15,18 +15,16 @@
> li > a {
border-left: 4px solid #EEE;
padding: 12px;
color: #777;
}
> .active > a {
border-color: $primary_color;
border-radius: 0;
background: #F1F1F1;
color: $style_color;
font-weight: bold;
text-shadow: 0 1px 1px #fff;
background: none;
color: #333;
font-weight: bolder;
}
&.nav-stacked-menu {
background: #FAFAFA;
li > a {
padding: 16px;
}
......@@ -36,6 +34,7 @@
&.nav-pills-small {
> li > a {
padding: 8px 12px;
font-size: 12px;
}
}
}
......
......@@ -20,6 +20,15 @@
label { width: 110px; }
.controls { margin-left: 130px; }
.form-actions { padding-left: 130px; background: #fff }
.visibility-levels {
.controls {
margin-bottom: 9px;
}
i {
color: inherit;
}
}
}
.broadcast-messages {
......
......@@ -70,7 +70,7 @@
font-size: 12px;
}
}
.old_line, .new_line {
.old_line, .new_line, .diff_line {
margin: 0px;
padding: 0px;
border: none;
......@@ -92,6 +92,15 @@
text-decoration: underline;
}
}
&.new {
background: #CFD;
}
&.old {
background: #FDD;
}
}
.diff_line {
padding: 0;
}
.line_holder {
&.old .old_line,
......@@ -122,6 +131,11 @@
color: #ccc;
background: #fafafa;
}
&.parallel {
display: table-cell;
overflow: hidden;
width: 50%;
}
}
}
.image {
......@@ -468,8 +482,8 @@ li.commit {
}
.commit-row-message {
color: #555;
font-weight: bolder;
color: #333;
font-weight: 500;
&:hover {
color: #444;
text-decoration: underline;
......@@ -478,13 +492,14 @@ li.commit {
}
.commit-row-info {
color: #777;
a {
color: #777;
}
.committed_ago {
float: right;
@extend .cgray;
}
}
......
......@@ -100,3 +100,21 @@
padding: 2px 5px;
}
}
.project-access-icon {
margin-left: 10px;
float: left;
margin-right: 15px;
font-size: 20px;
margin-bottom: 15px;
border: 1px solid #EEE;
padding: 8px 12px;
border-radius: 50px;
background: #f5f5f5;
width: 16px;
text-align: center;
i {
color: #BBB;
}
}
......@@ -46,8 +46,8 @@ header {
h1 {
margin: 0;
background: url('logo-black.png') no-repeat center 1px;
background-size: 38px;
background: url('logo-black.png') no-repeat center center;
background-size: 32px;
float: left;
height: 40px;
width: 40px;
......@@ -152,8 +152,8 @@ header {
.app_logo {
a {
h1 {
background: url('logo-white.png') no-repeat center 1px;
background-size: 38px;
background: url('logo-white.png') no-repeat center center;
background-size: 32px;
color: #fff;
text-shadow: 0 1px 1px #444;
}
......
......@@ -77,8 +77,8 @@ input.check_all_issues {
@media (min-width: 800px) { .issues_filters select { width: 160px; } }
@media (min-width: 1200px) { .issues_filters select { width: 220px; } }
@media (min-width: 800px) { .issues_bulk_update select { width: 120px; } }
@media (min-width: 1200px) { .issues_bulk_update select { width: 160px; } }
@media (min-width: 800px) { .issues_bulk_update .chosen-container { min-width: 120px; } }
@media (min-width: 1200px) { .issues_bulk_update .chosen-container { min-width: 160px; } }
.issues-holder {
.issues_filters {
......@@ -103,3 +103,19 @@ input.check_all_issues {
.participants {
margin-bottom: 10px;
}
.issues_bulk_update {
.chosen-container {
text-shadow: none;
}
}
.issue-search-form {
margin: 0;
height: 24px;
.issue_search {
border: 1px solid #DDD !important;
background-color: #f4f4f4;
}
}
......@@ -46,3 +46,10 @@ body.login-page{
margin: 2px;
}
}
.devise-errors {
h2 {
font-size: 14px;
color: #a00;
}
}
......@@ -130,6 +130,12 @@ ul.notes {
&.notes_line {
text-align: center;
padding: 10px 0;
background: #eee;
}
&.notes_line2 {
text-align: center;
padding: 10px 0;
border-left: 1px solid #ddd !important;
}
&.notes_content {
background-color: $white;
......@@ -270,10 +276,9 @@ ul.notes {
// preview/edit buttons
> a {
font-size: 24px;
padding: 4px;
position: absolute;
right: 10px;
right: 5px;
bottom: -60px;
}
.note_preview {
background: #f5f5f5;
......@@ -306,10 +311,8 @@ ul.notes {
.common-note-form {
margin: 0;
height: 140px;
background: #F9F9F9;
padding: 3px;
padding-bottom: 25px;
border: 1px solid #DDD;
}
......@@ -320,7 +323,7 @@ ul.notes {
padding: 0 5px;
.note-form-option {
margin-top: 10px;
margin-top: 8px;
margin-left: 30px;
@extend .pull-left;
}
......@@ -358,3 +361,7 @@ ul.notes {
.js-note-attachment-delete {
display: none;
}
.parallel-comment {
padding: 6px;
}
......@@ -42,3 +42,8 @@
margin-right: 12px;
}
.profile-avatar-form-option {
hr {
margin: 10px 0;
}
}
......@@ -19,6 +19,12 @@
padding-bottom: 25px;
margin-bottom: 30px;
&.empty-project {
border-bottom: 0px;
padding-bottom: 15px;
margin-bottom: 0px;
}
.project-home-title {
font-size: 18px;
color: #777;
......@@ -45,7 +51,7 @@
}
}
.public-label {
.visibility-level-label {
font-size: 14px;
background: #f1f1f1;
padding: 8px 10px;
......@@ -53,6 +59,10 @@
margin-left: 10px;
color: #888;
text-shadow: 0 1px 1px #FFF;
i {
color: inherit;
}
}
}
......@@ -61,13 +71,24 @@
border: 1px solid #E1E1E1;
@include border-radius(4px);
input[type="text"],
.btn {
margin-left: 3px;
border: none;
background: none;
@include border-radius(0px);
border-left: 1px solid #E1E1E1;
box-shadow: none;
padding: 6px 10px;
}
.btn {
float: left;
background: none;
color: #29b;
padding: 6px;
&:first-child {
@include border-radius-left(4px);
border-left: 0px;
}
&.active {
color: #333;
......@@ -76,20 +97,46 @@
}
input[type="text"] {
margin-left: 2px;
border: none;
border-radius: 0;
border-left: 1px solid #E1E1E1;
cursor: auto;
@extend .monospace;
box-shadow: none;
background: #FAFAFA;
padding: 6px 10px;
}
}
.project-public-holder {
.help-inline {
padding-top: 7px;
.project-visibility-level-holder {
.controls {
padding-bottom: 9px;
}
.controls {
input {
float: left;
}
.descr {
display: block;
margin-left: 1.5em;
&.restricted {
color: #888;
}
label {
float: none;
padding: 0;
margin: 0;
text-align: left;
}
}
.info {
display: block;
margin-top: 5px;
}
strong {
display: inline-block;
width: 4em;
}
}
i {
color: inherit;
}
}
......@@ -130,7 +177,8 @@ ul.nav.nav-projects-tabs {
margin: 0px;
}
.my-projects {
.my-projects,
.public-projects {
li {
.project-info {
margin-bottom: 10px;
......
require_relative "base_context"
module Files
class CreateContext < BaseContext
def execute
......@@ -19,13 +21,13 @@ module Files
file_path = path
unless file_name =~ Gitlab::Regex.path_regex
return error("Your changes could not be commited, because file name contains not allowed characters")
return error("Your changes could not be committed, because file name contains not allowed characters")
end
blob = repository.blob_at(ref, file_path)
if blob
return error("Your changes could not be commited, because file with such name exists")
return error("Your changes could not be committed, because file with such name exists")
end
new_file_action = Gitlab::Satellite::NewFileAction.new(current_user, project, ref, file_path)
......@@ -37,7 +39,7 @@ module Files
if created_successfully
success
else
error("Your changes could not be commited, because the file has been changed")
error("Your changes could not be committed, because the file has been changed")
end
end
end
......
require_relative "base_context"
module Files
class DeleteContext < BaseContext
def execute
......@@ -31,7 +33,7 @@ module Files
if deleted_successfully
success
else
error("Your changes could not be commited, because the file has been changed")
error("Your changes could not be committed, because the file has been changed")
end
end
end
......
require_relative "base_context"
module Files
class UpdateContext < BaseContext
def execute
......@@ -30,7 +32,7 @@ module Files
if created_successfully
success
else
error("Your changes could not be commited, because the file has been changed")
error("Your changes could not be committed, because the file has been changed")
end
end
end
......
......@@ -22,7 +22,7 @@ module Issues
opts[:milestone_id] = milestone_id if milestone_id.present?
opts[:assignee_id] = assignee_id if assignee_id.present?
issues = Issue.where(id: issues_ids).all
issues = Issue.where(id: issues_ids)
issues = issues.select { |issue| can?(current_user, :modify_issue, issue) }
issues.each do |issue|
......
......@@ -29,8 +29,26 @@ module Issues
if params[:milestone_id].present?
@issues = @issues.where(milestone_id: (params[:milestone_id] == '0' ? nil : params[:milestone_id]))
end
# Sort by :sort param
@issues = sort(@issues, params[:sort])
@issues
end
private
def sort(issues, condition)
case condition
when 'newest' then issues.except(:order).order('created_at DESC')
when 'oldest' then issues.except(:order).order('created_at ASC')
when 'recently_updated' then issues.except(:order).order('updated_at DESC')
when 'last_updated' then issues.except(:order).order('updated_at ASC')
when 'milestone_due_soon' then issues.except(:order).joins(:milestone).order("milestones.due_date ASC")
when 'milestone_due_later' then issues.except(:order).joins(:milestone).order("milestones.due_date DESC")
else issues
end
end
end
end
......@@ -8,6 +8,11 @@ module Projects
# get namespace id
namespace_id = params.delete(:namespace_id)
# check that user is allowed to set specified visibility_level
unless Gitlab::VisibilityLevel.allowed_for?(current_user, params[:visibility_level])
params.delete(:visibility_level)
end
# Load default feature settings
default_features = Gitlab.config.gitlab.default_projects_features
......@@ -17,7 +22,7 @@ module Projects
wall_enabled: default_features.wall,
snippets_enabled: default_features.snippets,
merge_requests_enabled: default_features.merge_requests,
public: default_features.public
visibility_level: default_features.visibility_level
}.stringify_keys
@project = Project.new(default_opts.merge(params))
......
......@@ -2,15 +2,15 @@ module Projects
class UpdateContext < BaseContext
def execute(role = :default)
params[:project].delete(:namespace_id)
params[:project].delete(:public) unless can?(current_user, :change_public_mode, project)
# check that user is allowed to set specified visibility_level
unless can?(current_user, :change_visibility_level, project) && Gitlab::VisibilityLevel.allowed_for?(current_user, params[:project][:visibility_level])
params[:project].delete(:visibility_level)
end
new_branch = params[:project].delete(:default_branch)
if project.repository.exists? && new_branch != project.repository.root_ref
GitlabShellWorker.perform_async(
:update_repository_head,
project.path_with_namespace,
new_branch
)
if project.repository.exists? && new_branch != project.default_branch
project.change_head(new_branch)
end
project.update_attributes(params[:project], as: role)
......
class SearchContext
attr_accessor :project_ids, :params
attr_accessor :project_ids, :current_user, :params
def initialize(project_ids, params)
@project_ids, @params = project_ids, params.dup
def initialize(project_ids, user, params)
@project_ids, @current_user, @params = project_ids, user, params.dup
end
def execute
......@@ -10,7 +10,8 @@ class SearchContext
query = Shellwords.shellescape(query) if query.present?
return result unless query.present?
result[:projects] = Project.where("projects.id in (?) OR projects.public = true", project_ids).search(query).limit(20)
visibility_levels = @current_user ? [ Gitlab::VisibilityLevel::INTERNAL, Gitlab::VisibilityLevel::PUBLIC ] : [ Gitlab::VisibilityLevel::PUBLIC ]
result[:projects] = Project.where("projects.id in (?) OR projects.visibility_level in (?)", project_ids, visibility_levels).search(query).limit(20)
# Search inside single project
single_project_search(Project.where(id: project_ids), query)
......
......@@ -7,8 +7,8 @@ class Admin::ProjectsController < Admin::ApplicationController
owner_id = params[:owner_id]
user = User.find_by_id(owner_id)
@projects = user ? user.owned_projects : Project.scoped
@projects = @projects.where(public: true) if params[:public_only].present?
@projects = user ? user.owned_projects : Project.all
@projects = @projects.where("visibility_level IN (?)", params[:visibility_levels]) if params[:visibility_levels].present?
@projects = @projects.with_push if params[:with_push].present?
@projects = @projects.abandoned if params[:abandoned].present?
@projects = @projects.search(params[:name]) if params[:name].present?
......
......@@ -2,8 +2,7 @@ class Admin::UsersController < Admin::ApplicationController
before_filter :user, only: [:show, :edit, :update, :destroy]
def index
@users = User.scoped
@users = @users.filter(params[:filter])
@users = User.filter(params[:filter])
@users = @users.search(params[:name]) if params[:name].present?
@users = @users.alphabetically.page(params[:page])
end
......
require 'gon'
class ApplicationController < ActionController::Base
before_filter :authenticate_user!
before_filter :reject_blocked!
......@@ -8,6 +10,7 @@ class ApplicationController < ActionController::Base
before_filter :dev_tools if Rails.env == 'development'
before_filter :default_headers
before_filter :add_gon_variables
before_filter :configure_permitted_parameters, if: :devise_controller?
protect_from_forgery
......@@ -82,6 +85,9 @@ class ApplicationController < ActionController::Base
if @project and can?(current_user, :read_project, @project)
@project
elsif current_user.nil?
@project = nil
authenticate_user!
else
@project = nil
render_404 and return
......@@ -103,7 +109,7 @@ class ApplicationController < ActionController::Base
end
def authorize_code_access!
return access_denied! unless can?(current_user, :download_code, project) or project.public?
return access_denied! unless can?(current_user, :download_code, project)
end
def authorize_push!
......@@ -193,4 +199,31 @@ class ApplicationController < ActionController::Base
def gitlab_ldap_access
Gitlab::LDAP::Access.new
end
# JSON for infinite scroll via Pager object
def pager_json(partial, count)
html = render_to_string(
partial,
layout: false,
formats: [:html]
)
render json: {
html: html,
count: count
}
end
def view_to_html_string(partial)
render_to_string(
partial,
layout: false,
formats: [:html]
)
end
def configure_permitted_parameters
devise_parameter_sanitizer.for(:sign_in) { |u| u.permit(:username, :email, :password) }
devise_parameter_sanitizer.for(:sign_up) { |u| u.permit(:username, :email, :name, :password, :password_confirmation) }
end
end
......@@ -22,7 +22,7 @@ class DashboardController < ApplicationController
respond_to do |format|
format.html
format.js
format.json { pager_json("events/_events", @events.count) }
format.atom { render layout: false }
end
end
......@@ -40,6 +40,7 @@ class DashboardController < ApplicationController
end
@projects = @projects.where(namespace_id: Group.find_by_name(params[:group])) if params[:group].present?
@projects = @projects.where(visibility_level: params[:visibility_level]) if params[:visibility_level].present?
@projects = @projects.includes(:namespace).sorted_by_activity
@labels = current_user.authorized_projects.tags_on(:labels)
......@@ -72,6 +73,6 @@ class DashboardController < ApplicationController
protected
def load_projects
@projects = current_user.authorized_projects.sorted_by_activity
@projects = current_user.authorized_projects.sorted_by_activity.non_archived
end
end
......@@ -40,7 +40,7 @@ class GroupsController < ApplicationController
respond_to do |format|
format.html
format.js
format.json { pager_json("events/_events", @events.count) }
format.atom { render layout: false }
end
end
......
class Profiles::AvatarsController < ApplicationController
layout "profile"
def destroy
@user = current_user
@user.remove_avatar!
@user.save
@user.reset_events_cache
redirect_to profile_path
end
end
......@@ -2,7 +2,7 @@ class Profiles::KeysController < ApplicationController
layout "profile"
def index
@keys = current_user.keys.order('id DESC').all
@keys = current_user.keys.order('id DESC')
end
def show
......
......@@ -13,6 +13,8 @@ class ProfilesController < ApplicationController
end
def update
params[:user].delete(:email) if @user.ldap_user?
if @user.update_attributes(params[:user])
flash[:notice] = "Profile was successfully updated"
else
......
......@@ -10,7 +10,7 @@ class Projects::ApplicationController < ApplicationController
id = params[:project_id] || params[:id]
@project = Project.find_with_namespace(id)
return if @project && @project.public
return if @project && @project.public?
end
super
......
......@@ -16,7 +16,7 @@ class Projects::BlobController < Projects::ApplicationController
result = Files::DeleteContext.new(@project, current_user, params, @ref, @path).execute
if result[:status] == :success
flash[:notice] = "Your changes have been successfully commited"
flash[:notice] = "Your changes have been successfully committed"
redirect_to project_tree_path(@project, @ref)
else
flash[:alert] = result[:error]
......
......@@ -16,7 +16,7 @@ class Projects::CommitsController < Projects::ApplicationController
respond_to do |format|
format.html # index.html.erb
format.js
format.json { pager_json("projects/commits/_commits", @commits.size) }
format.atom { render layout: false }
end
end
......
......@@ -7,7 +7,7 @@ class Projects::DeployKeysController < Projects::ApplicationController
layout "project_settings"
def index
@enabled_keys = @project.deploy_keys.all
@enabled_keys = @project.deploy_keys
@available_keys = available_keys - @enabled_keys
end
......
......@@ -10,7 +10,7 @@ class Projects::EditTreeController < Projects::BaseTreeController
result = Files::UpdateContext.new(@project, current_user, params, @ref, @path).execute
if result[:status] == :success
flash[:notice] = "Your changes have been successfully commited"
flash[:notice] = "Your changes have been successfully committed"
redirect_to project_blob_path(@project, @id)
else
flash[:alert] = result[:error]
......
......@@ -7,7 +7,7 @@ class Projects::HooksController < Projects::ApplicationController
layout "project_settings"
def index
@hooks = @project.hooks.all
@hooks = @project.hooks
@hook = ProjectHook.new
end
......@@ -18,7 +18,7 @@ class Projects::HooksController < Projects::ApplicationController
if @hook.valid?
redirect_to project_hooks_path(@project)
else
@hooks = @project.hooks.all
@hooks = @project.hooks
render :index
end
end
......
......@@ -11,7 +11,7 @@ class Projects::IssuesController < Projects::ApplicationController
# Allow modify issue
before_filter :authorize_modify_issue!, only: [:edit, :update]
respond_to :js, :html
respond_to :html
def index
terms = params['issue_search']
......@@ -23,11 +23,18 @@ class Projects::IssuesController < Projects::ApplicationController
assignee_id, milestone_id = params[:assignee_id], params[:milestone_id]
@assignee = @project.team.find(assignee_id) if assignee_id.present? && !assignee_id.to_i.zero?
@milestone = @project.milestones.find(milestone_id) if milestone_id.present? && !milestone_id.to_i.zero?
sort_param = params[:sort] || 'newest'
@sort = sort_param.humanize unless sort_param.empty?
respond_to do |format|
format.html # index.html.erb
format.js
format.html
format.atom { render layout: false }
format.json do
render json: {
html: view_to_html_string("projects/issues/_issues")
}
end
end
end
......@@ -45,10 +52,7 @@ class Projects::IssuesController < Projects::ApplicationController
@target_type = :issue
@target_id = @issue.id
respond_to do |format|
format.html
format.js
end
respond_with(@issue)
end
def create
......@@ -70,6 +74,7 @@ class Projects::IssuesController < Projects::ApplicationController
def update
@issue.update_attributes(params[:issue].merge(author_id_of_changes: current_user.id))
@issue.reset_events_cache
respond_to do |format|
format.js
......
......@@ -2,8 +2,8 @@ require 'gitlab/satellite/satellite'
class Projects::MergeRequestsController < Projects::ApplicationController
before_filter :module_enabled
before_filter :merge_request, only: [:edit, :update, :show, :commits, :diffs, :automerge, :automerge_check, :ci_status]
before_filter :closes_issues, only: [:edit, :update, :show, :commits, :diffs]
before_filter :merge_request, only: [:edit, :update, :show, :diffs, :automerge, :automerge_check, :ci_status]
before_filter :closes_issues, only: [:edit, :update, :show, :diffs]
before_filter :validates_merge_request, only: [:show, :diffs]
before_filter :define_show_vars, only: [:show, :diffs]
......@@ -26,8 +26,6 @@ class Projects::MergeRequestsController < Projects::ApplicationController
def show
respond_to do |format|
format.html
format.js
format.diff { render text: @merge_request.to_diff(current_user) }
format.patch { render text: @merge_request.to_patch(current_user) }
end
......@@ -44,6 +42,11 @@ class Projects::MergeRequestsController < Projects::ApplicationController
diff_line_count = Commit::diff_line_count(@merge_request.diffs)
@suppress_diff = Commit::diff_suppress?(@merge_request.diffs, diff_line_count) && !params[:force_show_diff]
@force_suppress_diff = Commit::diff_force_suppress?(@merge_request.diffs, diff_line_count)
respond_to do |format|
format.html
format.json { render json: { html: view_to_html_string("projects/merge_requests/show/_diffs") } }
end
end
def new
......@@ -76,9 +79,30 @@ class Projects::MergeRequestsController < Projects::ApplicationController
end
def update
# If we close MergeRequest we want to ignore validation
# so we can close broken one (Ex. fork project removed)
if params[:merge_request] == {"state_event"=>"close"}
@merge_request.allow_broken = true
if @merge_request.close
opts = { notice: 'Merge request was successfully closed.' }
else
opts = { alert: 'Failed to close merge request.' }
end
redirect_to [@merge_request.target_project, @merge_request], opts
return
end
# We dont allow change of source/target projects
# after merge request was created
params[:merge_request].delete(:source_project_id)
params[:merge_request].delete(:target_project_id)
if @merge_request.update_attributes(params[:merge_request].merge(author_id_of_changes: current_user.id))
@merge_request.reload_code
@merge_request.mark_as_unchecked
@merge_request.reset_events_cache
redirect_to [@merge_request.target_project, @merge_request], notice: 'Merge request was successfully updated.'
else
render "edit"
......@@ -157,14 +181,17 @@ class Projects::MergeRequestsController < Projects::ApplicationController
end
def validates_merge_request
# If source project was removed (Ex. mr from fork to origin)
return invalid_mr unless @merge_request.source_project
# Show git not found page
# if there is no saved commits between source & target branch
if @merge_request.commits.blank?
# and if source target doesn't exist
return invalid_mr unless @merge_request.target_project.repository.branch_names.include?(@merge_request.target_branch)
# and if target branch doesn't exist
return invalid_mr unless @merge_request.target_branch_exists?
# or if source branch doesn't exist
return invalid_mr unless @merge_request.source_project.repository.branch_names.include?(@merge_request.source_branch)
# or if source branch doesn't exist
return invalid_mr unless @merge_request.source_branch_exists?
end
end
......
......@@ -34,11 +34,6 @@ class Projects::MilestonesController < Projects::ApplicationController
@issues = @milestone.issues
@users = @milestone.participants.uniq
@merge_requests = @milestone.merge_requests
respond_to do |format|
format.html
format.js
end
end
def create
......
......@@ -9,7 +9,7 @@ class Projects::NewTreeController < Projects::BaseTreeController
result = Files::CreateContext.new(@project, current_user, params, @ref, file_path).execute
if result[:status] == :success
flash[:notice] = "Your changes have been successfully commited"
flash[:notice] = "Your changes have been successfully committed"
redirect_to project_blob_path(@project, File.join(@ref, file_path))
else
flash[:alert] = result[:error]
......
......@@ -14,7 +14,14 @@ class Projects::NotesController < Projects::ApplicationController
@discussions = discussions_from_notes
end
respond_with(@notes)
respond_to do |format|
format.html { redirect_to :back }
format.json do
render json: {
html: view_to_html_string("projects/notes/_notes")
}
end
end
end
def create
......@@ -32,6 +39,7 @@ class Projects::NotesController < Projects::ApplicationController
@note = @project.notes.find(params[:id])
return access_denied! unless can?(current_user, :admin_note, @note)
@note.destroy
@note.reset_events_cache
respond_to do |format|
format.js { render nothing: true }
......@@ -43,6 +51,7 @@ class Projects::NotesController < Projects::ApplicationController
return access_denied! unless can?(current_user, :admin_note, @note)
@note.update_attributes(params[:note])
@note.reset_events_cache
respond_to do |format|
format.js do
......
......@@ -5,7 +5,7 @@ class ProjectsController < ApplicationController
# Authorize
before_filter :authorize_read_project!, except: [:index, :new, :create]
before_filter :authorize_admin_project!, only: [:edit, :update, :destroy, :transfer]
before_filter :authorize_admin_project!, only: [:edit, :update, :destroy, :transfer, :archive, :unarchive]
before_filter :require_non_empty_project, only: [:blob, :tree, :graph]
layout 'navless', only: [:new, :create, :fork]
......@@ -55,7 +55,7 @@ class ProjectsController < ApplicationController
end
def show
return authenticate_user! unless @project.public || current_user
return authenticate_user! unless @project.public? || current_user
limit = (params[:limit] || 20).to_i
@events = @project.events.recent
......@@ -73,7 +73,7 @@ class ProjectsController < ApplicationController
render :show, layout: user_layout
end
end
format.js
format.json { pager_json("events/_events", @events.count) }
end
end
......@@ -116,6 +116,24 @@ class ProjectsController < ApplicationController
end
end
def archive
return access_denied! unless can?(current_user, :archive_project, project)
project.archive!
respond_to do |format|
format.html { redirect_to @project }
end
end
def unarchive
return access_denied! unless can?(current_user, :archive_project, project)
project.unarchive!
respond_to do |format|
format.html { redirect_to @project }
end
end
private
def set_title
......
......@@ -6,7 +6,7 @@ class Public::ProjectsController < ApplicationController
layout 'public'
def index
@projects = Project.public_only
@projects = Project.public_or_internal_only(current_user)
@projects = @projects.search(params[:search]) if params[:search].present?
@projects = @projects.includes(:namespace).order("namespaces.path, projects.name ASC").page(params[:page]).per(20)
end
......
......@@ -14,7 +14,7 @@ class SearchController < ApplicationController
project_ids.select! { |id| id == project_id.to_i}
end
result = SearchContext.new(project_ids, params).execute
result = SearchContext.new(project_ids, current_user, params).execute
@projects = result[:projects]
@merge_requests = result[:merge_requests]
......
......@@ -62,7 +62,7 @@ module ApplicationHelper
size = 40 if size.nil? || size <= 0
if !Gitlab.config.gravatar.enabled || user_email.blank?
'no_avatar.png'
'/assets/no_avatar.png'
else
gravatar_url = request.ssl? || gitlab_config.https ? Gitlab.config.gravatar.ssl_url : Gitlab.config.gravatar.plain_url
user_email.strip!
......@@ -72,7 +72,7 @@ module ApplicationHelper
def last_commit(project)
if project.repo_exists?
time_ago_in_words(project.repository.commit.committed_date) + " ago"
time_ago_with_tooltip(project.repository.commit.committed_date) + " ago"
else
"Never"
end
......@@ -136,9 +136,9 @@ module ApplicationHelper
Digest::SHA1.hexdigest string
end
def project_last_activity project
def project_last_activity(project)
if project.last_activity_at
time_ago_in_words(project.last_activity_at) + " ago"
time_ago_with_tooltip(project.last_activity_at, 'bottom', 'last_activity_time_ago') + " ago"
else
"Never"
end
......@@ -207,4 +207,22 @@ module ApplicationHelper
def broadcast_message
BroadcastMessage.current
end
def highlight_js(&block)
string = capture(&block)
content_tag :div, class: user_color_scheme_class do
Pygments::Lexer[:js].highlight(string).html_safe
end
end
def time_ago_with_tooltip(date, placement = 'top', html_class = 'time_ago')
capture_haml do
haml_tag :time, time_ago_in_words(date),
class: html_class, datetime: date, title: date.stamp("Aug 21, 2011 9:23pm"),
data: { toggle: 'tooltip', placement: placement }
haml_tag :script, "$('." + html_class + "').tooltip()"
end.html_safe
end
end
......@@ -105,6 +105,10 @@ module CommitsHelper
branches.sort.map { |branch| link_to(branch, project_tree_path(project, branch)) }.join(", ").html_safe
end
def get_old_file(project, commit, diff)
project.repository.blob_at(commit.parent_id, diff.old_path) if commit.parent_id
end
protected
# Private: Returns a link to a person. If the person has a matching user and
......
module CompareHelper
def compare_to_mr_button?
params[:from].present? && params[:to].present? &&
@project.merge_requests_enabled &&
params[:from].present? &&
params[:to].present? &&
@repository.branch_names.include?(params[:from]) &&
@repository.branch_names.include?(params[:to]) &&
params[:from] != params[:to] &&
......
......@@ -102,15 +102,11 @@ module EventsHelper
end
elsif event.note_project_snippet?
link_to(project_snippet_path(event.project, event.note_target)) do
content_tag :strong do
"#{event.note_target_type} ##{truncate event.note_target_id}"
end
"#{event.note_target_type} ##{truncate event.note_target_id}"
end
else
link_to event_note_target_path(event) do
content_tag :strong do
"#{event.note_target_type} ##{truncate event.note_target_iid}"
end
"#{event.note_target_type} ##{truncate event.note_target_iid}"
end
end
elsif event.wall_note?
......
......@@ -8,10 +8,14 @@ module IconsHelper
end
def public_icon
content_tag :i, nil, class: 'icon-globe cblue'
content_tag :i, nil, class: 'icon-globe'
end
def internal_icon
content_tag :i, nil, class: 'icon-shield'
end
def private_icon
content_tag :i, nil, class: 'icon-lock cgreen'
content_tag :i, nil, class: 'icon-lock'
end
end
......@@ -68,4 +68,12 @@ module IssuesHelper
false
end
end
def bulk_update_milestone_options
options_for_select(["None (backlog)", nil]) + options_from_collection_for_select(project_active_milestones, "id", "title", params[:milestone_id])
end
def bulk_update_assignee_options
options_for_select(["None (unassigned)", nil]) + options_from_collection_for_select(@project.team.members, "id", "name", params[:assignee_id])
end
end
......@@ -36,7 +36,7 @@ module MergeRequestsHelper
def merge_path_description(merge_request, separator)
if merge_request.for_fork?
"Project:Branches: #{@merge_request.source_project.path_with_namespace}:#{@merge_request.source_branch} #{separator} #{@merge_request.target_project.path_with_namespace}:#{@merge_request.target_branch}"
"Project:Branches: #{@merge_request.source_project_path}:#{@merge_request.source_branch} #{separator} #{@merge_request.target_project.path_with_namespace}:#{@merge_request.target_branch}"
else
"Branches: #{@merge_request.source_branch} #{separator} #{@merge_request.target_branch}"
end
......
......@@ -31,8 +31,14 @@ module NotesHelper
def note_timestamp(note)
# Shows the created at time and the updated at time if different
ts = "#{time_ago_in_words(note.created_at)} ago"
ts << content_tag(:small, " (Edited #{time_ago_in_words(note.updated_at)} ago)") if note.updated_at != note.created_at
ts = "#{time_ago_with_tooltip(note.created_at, 'bottom', 'note_created_ago')} ago"
if note.updated_at != note.created_at
ts << capture_haml do
haml_tag :small do
haml_concat " (Edited #{time_ago_with_tooltip(note.updated_at, 'bottom', 'note_edited_ago')} ago)"
end
end
end
ts.html_safe
end
end
......@@ -70,6 +70,8 @@ module ProjectsHelper
scope: params[:scope],
label_name: params[:label_name],
milestone_id: params[:milestone_id],
assignee_id: params[:assignee_id],
sort: params[:sort],
}
options = exist_opts.merge(options)
......@@ -80,7 +82,7 @@ module ProjectsHelper
end
def project_active_milestones
@project.milestones.active.order("due_date, title ASC").all
@project.milestones.active.order("due_date, title ASC")
end
def project_issues_trackers(current_tracker = nil)
......@@ -135,8 +137,8 @@ module ProjectsHelper
end
end
def repository_size
"#{@project.repository.size} MB"
def repository_size(project = nil)
"#{(project || @project).repository.size} MB"
rescue
# In order to prevent 500 error
# when application cannot allocate memory
......@@ -177,4 +179,12 @@ module ProjectsHelper
title
end
def default_url_to_repo
current_user ? @project.url_to_repo : @project.http_url_to_repo
end
def default_clone_protocol
current_user ? "ssh" : "http"
end
end
module SearchHelper
def search_autocomplete_source
return unless current_user
[
groups_autocomplete,
projects_autocomplete,
public_projects_autocomplete,
default_autocomplete,
project_autocomplete,
help_autocomplete
].flatten.to_json
].flatten.uniq do |item|
item[:label]
end.to_json
end
private
......@@ -71,7 +73,14 @@ module SearchHelper
# Autocomplete results for the current user's projects
def projects_autocomplete
current_user.authorized_projects.map do |p|
current_user.authorized_projects.non_archived.map do |p|
{ label: "project: #{simple_sanitize(p.name_with_namespace)}", url: project_path(p) }
end
end
# Autocomplete results for the current user's projects
def public_projects_autocomplete
Project.public_or_internal_only(current_user).non_archived.map do |p|
{ label: "project: #{simple_sanitize(p.name_with_namespace)}", url: project_path(p) }
end
end
......
module VisibilityLevelHelper
def visibility_level_color(level)
case level
when Gitlab::VisibilityLevel::PRIVATE
'cgreen'
when Gitlab::VisibilityLevel::INTERNAL
'camber'
when Gitlab::VisibilityLevel::PUBLIC
'cblue'
end
end
def visibility_level_description(level)
capture_haml do
haml_tag :span do
case level
when Gitlab::VisibilityLevel::PRIVATE
haml_concat "Project access must be granted explicitly for each user."
when Gitlab::VisibilityLevel::INTERNAL
haml_concat "The project can be cloned by"
haml_concat "any logged in user."
when Gitlab::VisibilityLevel::PUBLIC
haml_concat "The project can be cloned"
haml_concat "without any"
haml_concat "authentication."
end
end
end
end
def visibility_level_icon(level)
case level
when Gitlab::VisibilityLevel::PRIVATE
private_icon
when Gitlab::VisibilityLevel::INTERNAL
internal_icon
when Gitlab::VisibilityLevel::PUBLIC
public_icon
end
end
def visibility_level_label(level)
Project.visibility_levels.key(level)
end
def restricted_visibility_levels
current_user.is_admin? ? [] : gitlab_config.restricted_visibility_levels
end
end
......@@ -2,23 +2,27 @@ module Emails
module MergeRequests
def new_merge_request_email(recipient_id, merge_request_id)
@merge_request = MergeRequest.find(merge_request_id)
@project = @merge_request.project
mail(to: recipient(recipient_id), subject: subject("New merge request ##{@merge_request.iid}", @merge_request.title))
end
def reassigned_merge_request_email(recipient_id, merge_request_id, previous_assignee_id)
@merge_request = MergeRequest.find(merge_request_id)
@previous_assignee = User.find_by_id(previous_assignee_id) if previous_assignee_id
@project = @merge_request.project
mail(to: recipient(recipient_id), subject: subject("Changed merge request ##{@merge_request.iid}", @merge_request.title))
end
def closed_merge_request_email(recipient_id, merge_request_id, updated_by_user_id)
@merge_request = MergeRequest.find(merge_request_id)
@updated_by = User.find updated_by_user_id
@project = @merge_request.project
mail(to: recipient(recipient_id), subject: subject("Closed merge request ##{@merge_request.iid}", @merge_request.title))
end
def merged_merge_request_email(recipient_id, merge_request_id)
@merge_request = MergeRequest.find(merge_request_id)
@project = @merge_request.project
mail(to: recipient(recipient_id), subject: subject("Accepted merge request ##{@merge_request.iid}", @merge_request.title))
end
end
......
......@@ -13,5 +13,15 @@ module Emails
mail(to: @user.email,
subject: subject("Project was moved"))
end
def repository_push_email(project_id, recipient, author_id, branch, compare)
@project = Project.find(project_id)
@author = User.find(author_id)
@commits = Commit.decorate(compare.commits)
@diffs = compare.diffs
@branch = branch
mail(to: recipient, subject: subject("New push to repository"))
end
end
end
......@@ -16,6 +16,7 @@ class Notify < ActionMailer::Base
default_url_options[:script_name] = Gitlab.config.gitlab.relative_url_root
default from: Gitlab.config.gitlab.email_from
default reply_to: "noreply@#{Gitlab.config.gitlab.host}"
# Just send email with 3 seconds delay
def self.delay
......
......@@ -29,7 +29,7 @@ class Ability
nil
end
if project && project.public
if project && project.public?
[
:read_project,
:read_wiki,
......@@ -59,37 +59,41 @@ class Ability
# Rules based on role in project
if team.masters.include?(user)
rules << project_master_rules
rules += project_master_rules
elsif team.developers.include?(user)
rules << project_dev_rules
rules += project_dev_rules
elsif team.reporters.include?(user)
rules << project_report_rules
rules += project_report_rules
elsif team.guests.include?(user)
rules << project_guest_rules
rules += project_guest_rules
end
if project.public?
rules << public_project_rules
if project.public? || project.internal?
rules += public_project_rules
end
if project.owner == user || user.admin?
rules << project_admin_rules
rules += project_admin_rules
end
if project.group && project.group.has_owner?(user)
rules << project_admin_rules
rules += project_admin_rules
end
rules.flatten
if project.archived?
rules -= project_archived_rules
end
rules
end
def public_project_rules
project_guest_rules + [
:download_code,
:fork_project,
:fork_project
]
end
......@@ -125,6 +129,16 @@ class Ability
]
end
def project_archived_rules
[
:write_merge_request,
:push_code,
:push_code_to_protected_branches,
:modify_merge_request,
:admin_merge_request
]
end
def project_master_rules
project_dev_rules + [
:push_code_to_protected_branches,
......@@ -145,9 +159,10 @@ class Ability
def project_admin_rules
project_master_rules + [
:change_namespace,
:change_public_mode,
:change_visibility_level,
:rename_project,
:remove_project
:remove_project,
:archive_project
]
end
......@@ -160,7 +175,7 @@ class Ability
# Only group owner and administrators can manage group
if group.has_owner?(user) || user.admin?
rules << [
rules += [
:manage_group,
:manage_namespace
]
......@@ -174,7 +189,7 @@ class Ability
# Only namespace owner and administrators can manage it
if namespace.owner == user || user.admin?
rules << [
rules += [
:manage_namespace
]
end
......
......@@ -111,4 +111,11 @@ module Issuable
end
users.concat(mentions.reduce([], :|)).uniq
end
def to_hook_data
{
object_kind: self.class.name.underscore,
object_attributes: self.attributes
}
end
end
......@@ -18,7 +18,7 @@ class Event < ActiveRecord::Base
attr_accessible :project, :action, :data, :author_id, :project_id,
:target_id, :target_type
default_scope where("author_id IS NOT NULL")
default_scope { where.not(author_id: nil) }
CREATED = 1
UPDATED = 2
......@@ -223,7 +223,7 @@ class Event < ActiveRecord::Base
# Max 20 commits from push DESC
def commits
@commits ||= data[:commits].reverse
@commits ||= (data[:commits] || []).reverse
end
def commits_count
......
......@@ -33,7 +33,7 @@ class GollumWiki
end
def http_url_to_repo
http_url = [Gitlab.config.gitlab.url, "/", path_with_namespace, ".git"].join('')
[Gitlab.config.gitlab.url, "/", path_with_namespace, ".git"].join('')
end
# Returns the Gollum::Wiki object.
......
......@@ -36,7 +36,7 @@ class Group < Namespace
def add_users(user_ids, group_access)
user_ids.compact.each do |user_id|
user = self.users_groups.find_or_initialize_by_user_id(user_id)
user = self.users_groups.find_or_initialize_by(user_id: user_id)
user.update_attributes(group_access: group_access)
end
end
......
......@@ -28,7 +28,7 @@ class Issue < ActiveRecord::Base
scope :of_group, ->(group) { where(project_id: group.project_ids) }
scope :of_user_team, ->(team) { where(project_id: team.project_ids, assignee_id: team.member_ids) }
scope :opened, -> { with_state(:opened) }
scope :opened, -> { with_state(:opened, :reopened) }
scope :closed, -> { with_state(:closed) }
attr_accessible :title, :assignee_id, :position, :description,
......@@ -56,12 +56,23 @@ class Issue < ActiveRecord::Base
state :closed
end
# Both open and reopened issues should be listed as opened
scope :opened, -> { with_state(:opened, :reopened) }
# Mentionable overrides.
def gfm_reference
"issue ##{iid}"
end
# Reset issue events cache
#
# Since we do cache @event we need to reset cache in special cases:
# * when an issue is updated
# Events cache stored like events/23-20130109142513.
# The cache key includes updated_at timestamp.
# Thus it will automatically generate a new fragment
# when the event is updated because the key changes.
def reset_events_cache
Event.where(target_id: self.id, target_type: 'Issue').
order('id DESC').limit(100).
update_all(updated_at: Time.now)
end
end
......@@ -35,6 +35,10 @@ class MergeRequest < ActiveRecord::Base
attr_accessor :should_remove_source_branch
# When this attribute is true some MR validation is ignored
# It allows us to close or modify broken merge requests
attr_accessor :allow_broken
state_machine :state, initial: :opened do
event :close do
transition [:reopened, :opened] => :closed
......@@ -80,7 +84,7 @@ class MergeRequest < ActiveRecord::Base
serialize :st_commits
serialize :st_diffs
validates :source_project, presence: true
validates :source_project, presence: true, unless: :allow_broken
validates :source_branch, presence: true
validates :target_project, presence: true
validates :target_branch, presence: true
......@@ -262,7 +266,7 @@ class MergeRequest < ActiveRecord::Base
# Return the set of issues that will be closed if this merge request is accepted.
def closes_issues
if target_branch == project.default_branch
unmerged_commits.map { |c| c.closes_issues(project) }.flatten.uniq.sort_by(&:id)
commits.map { |c| c.closes_issues(project) }.flatten.uniq.sort_by(&:id)
else
[]
end
......@@ -273,6 +277,48 @@ class MergeRequest < ActiveRecord::Base
"merge request !#{iid}"
end
def target_project_path
if target_project
target_project.path_with_namespace
else
"(removed)"
end
end
def source_project_path
if source_project
source_project.path_with_namespace
else
"(removed)"
end
end
def source_branch_exists?
return false unless self.source_project
self.source_project.repository.branch_names.include?(self.source_branch)
end
def target_branch_exists?
return false unless self.target_project
self.target_project.repository.branch_names.include?(self.target_branch)
end
# Reset merge request events cache
#
# Since we do cache @event we need to reset cache in special cases:
# * when a merge request is updated
# Events cache stored like events/23-20130109142513.
# The cache key includes updated_at timestamp.
# Thus it will automatically generate a new fragment
# when the event is updated because the key changes.
def reset_events_cache
Event.where(target_id: self.id, target_type: 'MergeRequest').
order('id DESC').limit(100).
update_all(updated_at: Time.now)
end
private
def dump_commits(commits)
......
......@@ -157,7 +157,8 @@ class Note < ActiveRecord::Base
# otherwise false is returned
def downvote?
votable? && (note.start_with?('-1') ||
note.start_with?(':-1:')
note.start_with?(':-1:') ||
note.start_with?(':thumbsdown:')
)
end
......@@ -206,7 +207,8 @@ class Note < ActiveRecord::Base
# otherwise false is returned
def upvote?
votable? && (note.start_with?('+1') ||
note.start_with?(':+1:')
note.start_with?(':+1:') ||
note.start_with?(':thumbsup:')
)
end
......@@ -237,4 +239,19 @@ class Note < ActiveRecord::Base
def noteable_type=(sType)
super(sType.to_s.classify.constantize.base_class.to_s)
end
# Reset notes events cache
#
# Since we do cache @event we need to reset cache in special cases:
# * when a note is updated
# * when a note is removed
# Events cache stored like events/23-20130109142513.
# The cache key includes updated_at timestamp.
# Thus it will automatically generate a new fragment
# when the event is updated because the key changes.
def reset_events_cache
Event.where(target_id: self.id, target_type: 'Note').
order('id DESC').limit(100).
update_all(updated_at: Time.now)
end
end
......@@ -14,24 +14,25 @@
# merge_requests_enabled :boolean default(TRUE), not null
# wiki_enabled :boolean default(TRUE), not null
# namespace_id :integer
# public :boolean default(FALSE), not null
# issues_tracker :string(255) default("gitlab"), not null
# issues_tracker_id :string(255)
# snippets_enabled :boolean default(TRUE), not null
# last_activity_at :datetime
# imported :boolean default(FALSE), not null
# import_url :string(255)
# visibility_level :integer default(0), not null
#
class Project < ActiveRecord::Base
include Gitlab::ShellAdapter
include Gitlab::VisibilityLevel
extend Enumerize
ActsAsTaggableOn.strict_case_match = true
attr_accessible :name, :path, :description, :issues_tracker, :label_list,
:issues_enabled, :wall_enabled, :merge_requests_enabled, :snippets_enabled, :issues_tracker_id,
:wiki_enabled, :public, :import_url, :last_activity_at, as: [:default, :admin]
:wiki_enabled, :visibility_level, :import_url, :last_activity_at, as: [:default, :admin]
attr_accessible :namespace_id, :creator_id, as: :admin
......@@ -41,23 +42,29 @@ class Project < ActiveRecord::Base
# Relations
belongs_to :creator, foreign_key: "creator_id", class_name: "User"
belongs_to :group, foreign_key: "namespace_id", conditions: "type = 'Group'"
belongs_to :group, -> { where(type: Group) }, foreign_key: "namespace_id"
belongs_to :namespace
has_one :last_event, class_name: 'Event', order: 'events.created_at DESC', foreign_key: 'project_id'
has_one :last_event, -> {order 'events.created_at DESC'}, class_name: 'Event', foreign_key: 'project_id'
has_one :gitlab_ci_service, dependent: :destroy
has_one :campfire_service, dependent: :destroy
has_one :emails_on_push_service, dependent: :destroy
has_one :pivotaltracker_service, dependent: :destroy
has_one :hipchat_service, dependent: :destroy
has_one :flowdock_service, dependent: :destroy
has_one :assembla_service, dependent: :destroy
has_one :forked_project_link, dependent: :destroy, foreign_key: "forked_to_project_id"
has_one :forked_from_project, through: :forked_project_link
# Merge Requests for target project should be removed with it
has_many :merge_requests, dependent: :destroy, foreign_key: "target_project_id"
# Merge requests from source project should be kept when source project was removed
has_many :fork_merge_requests, foreign_key: "source_project_id", class_name: MergeRequest
has_many :issues, -> { order "state DESC, created_at DESC" }, dependent: :destroy
has_many :services, dependent: :destroy
has_many :events, dependent: :destroy
has_many :merge_requests, dependent: :destroy, foreign_key: "target_project_id"
has_many :fork_merge_requests,dependent: :destroy, foreign_key: "source_project_id", class_name: MergeRequest
has_many :issues, dependent: :destroy, order: "state DESC, created_at DESC"
has_many :milestones, dependent: :destroy
has_many :notes, dependent: :destroy
has_many :snippets, dependent: :destroy, class_name: "ProjectSnippet"
......@@ -77,8 +84,8 @@ class Project < ActiveRecord::Base
delegate :members, to: :team, prefix: true
# Validations
validates :creator, presence: true
validates :description, length: { within: 0..2000 }
validates :creator, presence: true, on: :create
validates :description, length: { maximum: 2000 }, allow_blank: true
validates :name, presence: true, length: { within: 0..255 },
format: { with: Gitlab::Regex.project_name_regex,
message: "only letters, digits, spaces & '_' '-' '.' allowed. Letter or digit should be first" }
......@@ -88,7 +95,7 @@ class Project < ActiveRecord::Base
message: "only letters, digits & '_' '-' '.' allowed. Letter or digit should be first" }
validates :issues_enabled, :wall_enabled, :merge_requests_enabled,
:wiki_enabled, inclusion: { in: [true, false] }
validates :issues_tracker_id, length: { within: 0..255 }
validates :issues_tracker_id, length: { maximum: 255 }, allow_blank: true
validates :namespace, presence: true
validates_uniqueness_of :name, scope: :namespace_id
......@@ -110,7 +117,10 @@ class Project < ActiveRecord::Base
scope :sorted_by_activity, -> { reorder("projects.last_activity_at DESC") }
scope :personal, ->(user) { where(namespace_id: user.namespace_id) }
scope :joined, ->(user) { where("namespace_id != ?", user.namespace_id) }
scope :public_only, -> { where(public: true) }
scope :public_only, -> { where(visibility_level: PUBLIC) }
scope :public_or_internal_only, ->(user) { where("visibility_level IN (:levels)", levels: user ? [ INTERNAL, PUBLIC ] : [ PUBLIC ]) }
scope :non_archived, -> { where(archived: false) }
enumerize :issues_tracker, in: (Gitlab.config.issues_tracker.keys).append(:gitlab), default: :gitlab
......@@ -128,7 +138,7 @@ class Project < ActiveRecord::Base
end
def search query
joins(:namespace).where("projects.name LIKE :query OR projects.path LIKE :query OR namespaces.name LIKE :query OR projects.description LIKE :query", query: "%#{query}%")
joins(:namespace).where("projects.archived = ?", false).where("projects.name LIKE :query OR projects.path LIKE :query OR namespaces.name LIKE :query OR projects.description LIKE :query", query: "%#{query}%")
end
def find_with_namespace(id)
......@@ -142,6 +152,10 @@ class Project < ActiveRecord::Base
where(path: id, namespace_id: nil).last
end
end
def visibility_levels
Gitlab::VisibilityLevel.options
end
end
def team
......@@ -227,7 +241,7 @@ class Project < ActiveRecord::Base
end
def available_services_names
%w(gitlab_ci campfire hipchat pivotaltracker flowdock)
%w(gitlab_ci campfire hipchat pivotaltracker flowdock assembla emails_on_push)
end
def gitlab_ci?
......@@ -294,8 +308,10 @@ class Project < ActiveRecord::Base
ProjectTransferService.new.transfer(self, new_namespace)
end
def execute_hooks(data)
hooks.each { |hook| hook.async_execute(data) }
def execute_hooks(data, hooks_scope = :push_hooks)
hooks.send(hooks_scope).each do |hook|
hook.async_execute(data)
end
end
def execute_services(data)
......@@ -312,14 +328,14 @@ class Project < ActiveRecord::Base
c_ids = self.repository.commits_between(oldrev, newrev).map(&:id)
# Update code for merge requests into project between project branches
mrs = self.merge_requests.opened.by_branch(branch_name).all
mrs = self.merge_requests.opened.by_branch(branch_name).to_a
# Update code for merge requests between project and project fork
mrs += self.fork_merge_requests.opened.by_branch(branch_name).all
mrs += self.fork_merge_requests.opened.by_branch(branch_name).to_a
mrs.each { |merge_request| merge_request.reload_code; merge_request.mark_as_unchecked }
# Close merge requests
mrs = self.merge_requests.opened.where(target_branch: branch_name).all
mrs = self.merge_requests.opened.where(target_branch: branch_name).to_a
mrs = mrs.select(&:last_commit).select { |mr| c_ids.include?(mr.last_commit.id) }
mrs.each { |merge_request| merge_request.merge!(user.id) }
......@@ -388,7 +404,7 @@ class Project < ActiveRecord::Base
end
def http_url_to_repo
http_url = [Gitlab.config.gitlab.url, "/", path_with_namespace, ".git"].join('')
[Gitlab.config.gitlab.url, "/", path_with_namespace, ".git"].join('')
end
# Check if current branch name is marked as protected in the system
......@@ -453,4 +469,26 @@ class Project < ActiveRecord::Base
def default_branch
@default_branch ||= repository.root_ref if repository.exists?
end
def reload_default_branch
@default_branch = nil
default_branch
end
def visibility_level_field
visibility_level
end
def archive!
update_attribute(:archived, true)
end
def unarchive!
update_attribute(:archived, false)
end
def change_head(branch)
gitlab_shell.update_repository_head(self.path_with_namespace, branch)
reload_default_branch
end
end
......@@ -2,15 +2,24 @@
#
# Table name: web_hooks
#
# id :integer not null, primary key
# url :string(255)
# project_id :integer
# created_at :datetime not null
# updated_at :datetime not null
# type :string(255) default("ProjectHook")
# service_id :integer
# id :integer not null, primary key
# url :string(255)
# project_id :integer
# created_at :datetime not null
# updated_at :datetime not null
# type :string(255) default("ProjectHook")
# service_id :integer
# push_events :boolean default(TRUE), not null
# issues_events :boolean default(FALSE), not null
# merge_requests_events :boolean default(FALSE), not null
#
class ProjectHook < WebHook
belongs_to :project
attr_accessible :push_events, :issues_events, :merge_requests_events
scope :push_hooks, -> { where(push_events: true) }
scope :issue_hooks, -> { where(issues_events: true) }
scope :merge_request_hooks, -> { where(merge_requests_events: true) }
end
# == Schema Information
#
# Table name: services
#
# id :integer not null, primary key
# type :string(255)
# title :string(255)
# token :string(255)
# project_id :integer not null
# created_at :datetime not null
# updated_at :datetime not null
# active :boolean default(FALSE), not null
# project_url :string(255)
# subdomain :string(255)
# room :string(255)
#
class AssemblaService < Service
include HTTParty
validates :token, presence: true, if: :activated?
def title
'Assembla'
end
def description
'Project Management Software (Source Commits Endpoint)'
end
def to_param
'assembla'
end
def fields
[
{ type: 'text', name: 'token', placeholder: '' }
]
end
def execute(push)
url = "https://atlas.assembla.com/spaces/ouposp/github_tool?secret_key=#{token}"
AssemblaService.post(url, body: { payload: push }.to_json, headers: { 'Content-Type' => 'application/json' })
end
end
# == Schema Information
#
# Table name: services
#
# id :integer not null, primary key
# type :string(255)
# title :string(255)
# token :string(255)
# project_id :integer not null
# created_at :datetime not null
# updated_at :datetime not null
# active :boolean default(FALSE), not null
# project_url :string(255)
# subdomain :string(255)
# room :string(255)
#
class EmailsOnPushService < Service
attr_accessible :recipients
validates :recipients, presence: true, if: :activated?
def title
'Emails on push'
end
def description
'Email the commits and diff of each push to a list of recipients.'
end
def to_param
'emails_on_push'
end
def execute(push_data)
EmailsOnPushWorker.perform_async(project_id, recipients, push_data)
end
def fields
[
{ type: 'textarea', name: 'recipients', placeholder: 'Emails separated by whitespace' },
]
end
end
......@@ -87,9 +87,8 @@ class ProjectTeam
def import(source_project)
target_project = project
source_team = source_project.users_projects.all
target_team = target_project.users_projects.all
target_user_ids = target_team.map(&:user_id)
source_team = source_project.users_projects.to_a
target_user_ids = target_project.users_projects.pluck(:user_id)
source_team.reject! do |tm|
# Skip if user already present in team
......
......@@ -133,6 +133,7 @@ class Repository
Rails.cache.delete(cache_key(:tag_names))
Rails.cache.delete(cache_key(:commit_count))
Rails.cache.delete(cache_key(:graph_log))
Rails.cache.delete(cache_key(:readme))
end
def graph_log
......@@ -159,4 +160,10 @@ class Repository
def blob_at(sha, path)
Gitlab::Git::Blob.find(self, sha, path)
end
def readme
Rails.cache.fetch(cache_key(:readme)) do
Tree.new(self, self.root_ref).readme
end
end
end
......@@ -2,13 +2,16 @@
#
# Table name: web_hooks
#
# id :integer not null, primary key
# url :string(255)
# project_id :integer
# created_at :datetime not null
# updated_at :datetime not null
# type :string(255) default("ProjectHook")
# service_id :integer
# id :integer not null, primary key
# url :string(255)
# project_id :integer
# created_at :datetime not null
# updated_at :datetime not null
# type :string(255) default("ProjectHook")
# service_id :integer
# push_events :boolean default(TRUE), not null
# issues_events :boolean default(FALSE), not null
# merge_requests_events :boolean default(FALSE), not null
#
class ServiceHook < WebHook
......
......@@ -2,13 +2,16 @@
#
# Table name: web_hooks
#
# id :integer not null, primary key
# url :string(255)
# project_id :integer
# created_at :datetime not null
# updated_at :datetime not null
# type :string(255) default("ProjectHook")
# service_id :integer
# id :integer not null, primary key
# url :string(255)
# project_id :integer
# created_at :datetime not null
# updated_at :datetime not null
# type :string(255) default("ProjectHook")
# service_id :integer
# push_events :boolean default(TRUE), not null
# issues_events :boolean default(FALSE), not null
# merge_requests_events :boolean default(FALSE), not null
#
class SystemHook < WebHook
......
......@@ -41,6 +41,7 @@
# confirmed_at :datetime
# confirmation_sent_at :datetime
# unconfirmed_email :string(255)
# hide_no_ssh_key :boolean default(FALSE), not null
#
require 'carrierwave/orm/activerecord'
......@@ -52,7 +53,7 @@ class User < ActiveRecord::Base
attr_accessible :email, :password, :password_confirmation, :remember_me, :bio, :name, :username,
:skype, :linkedin, :twitter, :color_scheme_id, :theme_id, :force_random_password,
:extern_uid, :provider, :password_expires_at, :avatar,
:extern_uid, :provider, :password_expires_at, :avatar, :hide_no_ssh_key,
as: [:default, :admin]
attr_accessible :projects_limit, :can_create_group,
......@@ -72,7 +73,7 @@ class User < ActiveRecord::Base
#
# Namespace for personal projects
has_one :namespace, dependent: :destroy, foreign_key: :owner_id, class_name: "Namespace", conditions: 'type IS NULL'
has_one :namespace, -> { where type: nil }, dependent: :destroy, foreign_key: :owner_id, class_name: "Namespace"
# Profile
has_many :keys, dependent: :destroy
......@@ -80,8 +81,7 @@ class User < ActiveRecord::Base
# Groups
has_many :users_groups, dependent: :destroy
has_many :groups, through: :users_groups
has_many :owned_groups, through: :users_groups, source: :group, conditions: { users_groups: { group_access: UsersGroup::OWNER } }
has_many :owned_groups, -> { where users_groups: { group_access: UsersGroup::OWNER } }, through: :users_groups, source: :group
# Projects
has_many :groups_projects, through: :groups, source: :projects
has_many :personal_projects, through: :namespace, source: :projects
......@@ -94,7 +94,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 :recent_events, foreign_key: :author_id, class_name: "Event", order: "id DESC"
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"
......@@ -104,7 +104,7 @@ class User < ActiveRecord::Base
#
validates :name, presence: true
validates :email, presence: true, format: { with: /\A([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})\Z/ }
validates :bio, length: { within: 0..255 }
validates :bio, length: { maximum: 255 }, allow_blank: true
validates :extern_uid, allow_blank: true, uniqueness: {scope: :provider}
validates :projects_limit, presence: true, numericality: {greater_than_or_equal_to: 0}
validates :username, presence: true, uniqueness: true,
......@@ -164,7 +164,7 @@ class User < ActiveRecord::Base
scope :alphabetically, -> { order('name ASC') }
scope :in_team, ->(team){ where(id: team.member_ids) }
scope :not_in_team, ->(team){ where('users.id NOT IN (:ids)', ids: team.member_ids) }
scope :not_in_project, ->(project) { project.users.present? ? where("id not in (:ids)", ids: project.users.map(&:id) ) : scoped }
scope :not_in_project, ->(project) { project.users.present? ? where("id not in (:ids)", ids: project.users.map(&:id) ) : all }
scope :without_projects, -> { where('id NOT IN (SELECT DISTINCT(user_id) FROM users_projects)') }
scope :ldap, -> { where(provider: 'ldap') }
......@@ -199,7 +199,7 @@ class User < ActiveRecord::Base
end
def by_username_or_id(name_or_id)
where('username = ? OR id = ?', name_or_id, name_or_id).first
where('users.username = ? OR users.id = ?', name_or_id, name_or_id.to_i).first
end
def build_user(attrs = {}, options= {})
......@@ -377,7 +377,7 @@ class User < ActiveRecord::Base
end
def accessible_deploy_keys
DeployKey.in_projects(self.authorized_projects).uniq
DeployKey.in_projects(self.authorized_projects.pluck(:id)).uniq
end
def created_by
......@@ -413,4 +413,18 @@ class User < ActiveRecord::Base
project.namespace != namespace &&
project.project_member(self)
end
# Reset project events cache related to this user
#
# Since we do cache @event we need to reset cache in special cases:
# * when the user changes their avatar
# Events cache stored like events/23-20130109142513.
# The cache key includes updated_at timestamp.
# Thus it will automatically generate a new fragment
# when the event is updated because the key changes.
def reset_events_cache
Event.where(author_id: self.id).
order('id DESC').limit(1000).
update_all(updated_at: Time.now)
end
end
......@@ -2,13 +2,16 @@
#
# Table name: web_hooks
#
# id :integer not null, primary key
# url :string(255)
# project_id :integer
# created_at :datetime not null
# updated_at :datetime not null
# type :string(255) default("ProjectHook")
# service_id :integer
# id :integer not null, primary key
# url :string(255)
# project_id :integer
# created_at :datetime not null
# updated_at :datetime not null
# type :string(255) default("ProjectHook")
# service_id :integer
# push_events :boolean default(TRUE), not null
# issues_events :boolean default(FALSE), not null
# merge_requests_events :boolean default(FALSE), not null
#
class WebHook < ActiveRecord::Base
......
class IssueObserver < BaseObserver
def after_create(issue)
notification.new_issue(issue, current_user)
issue.create_cross_references!(issue.project, current_user)
execute_hooks(issue)
end
def after_close(issue, transition)
notification.close_issue(issue, current_user)
create_note(issue)
execute_hooks(issue)
end
def after_reopen(issue, transition)
create_note(issue)
execute_hooks(issue)
end
def after_update(issue)
......@@ -21,6 +22,7 @@ class IssueObserver < BaseObserver
end
issue.notice_added_references(issue.project, current_user)
execute_hooks(issue)
end
protected
......@@ -29,4 +31,8 @@ class IssueObserver < BaseObserver
def create_note(issue)
Note.create_status_change_note(issue, issue.project, current_user, issue.state, current_commit)
end
def execute_hooks(issue)
issue.project.execute_hooks(issue.to_hook_data, :issue_hooks)
end
end
......@@ -7,15 +7,15 @@ class MergeRequestObserver < ActivityObserver
end
notification.new_merge_request(merge_request, current_user)
merge_request.create_cross_references!(merge_request.project, current_user)
execute_hooks(merge_request)
end
def after_close(merge_request, transition)
create_event(merge_request, Event::CLOSED)
Note.create_status_change_note(merge_request, merge_request.target_project, current_user, merge_request.state, nil)
notification.close_mr(merge_request, current_user)
create_note(merge_request)
execute_hooks(merge_request)
end
def after_merge(merge_request, transition)
......@@ -31,17 +31,21 @@ class MergeRequestObserver < ActivityObserver
action: Event::MERGED,
author_id: merge_request.author_id_of_changes
)
execute_hooks(merge_request)
end
def after_reopen(merge_request, transition)
create_event(merge_request, Event::REOPENED)
Note.create_status_change_note(merge_request, merge_request.target_project, current_user, merge_request.state, nil)
create_note(merge_request)
execute_hooks(merge_request)
end
def after_update(merge_request)
notification.reassigned_merge_request(merge_request, current_user) if merge_request.is_being_reassigned?
merge_request.notice_added_references(merge_request.project, current_user)
execute_hooks(merge_request)
end
def create_event(record, status)
......@@ -53,4 +57,17 @@ class MergeRequestObserver < ActivityObserver
author_id: current_user.id
)
end
private
# Create merge request note with service comment like 'Status changed to closed'
def create_note(merge_request)
Note.create_status_change_note(merge_request, merge_request.target_project, current_user, merge_request.state, nil)
end
def execute_hooks(merge_request)
if merge_request.project
merge_request.project.execute_hooks(merge_request.to_hook_data, :merge_request_hooks)
end
end
end
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment