diff options
190 files changed, 3490 insertions, 1199 deletions
diff --git a/CHANGELOG b/CHANGELOG index 0416a51ef7c..f26466f9da8 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -8,9 +8,22 @@ v 5.3.0 - Respect newlines in wall messages - Generate the Rails secret token on first run - Rename repo feature - - Init.d: remove gitlab.scoket on service starta + - Init.d: remove gitlab.socket on service start - Api: added teams api - Api: Prevent blob content being escaped + - Api: Smart deploy key add behaviour + - Api: projets/owned.json return user owned project + - Fix bug with team assignation on project from #4109 + - Advanced snippets: public/private, project/personal (Andrew Kulakov) + - Repository Graphs (Karlo Nicholas T. Soriano) + - Fix dashboard lost if comment on commit + - Update gitlab-grack. Fixes issue with --depth option + - Fix project events duplicate on project page + - Fix postgres error when displaying network graph. + - Fix dashboard event filter when navigate via turbolinks + - init.d: Ensure socket is removed before starting service + - Admin area: Style teams:index, group:show pages + - Own page for failed forking v 5.2.0 - Turbolinks @@ -23,10 +23,10 @@ gem 'omniauth-github' # Extracting information from a git repository # Provide access to Gitlab::Git library -gem 'gitlab_git', '~> 1.2.1' +gem 'gitlab_git', '~> 1.3.0' # Ruby/Rack Git Smart-HTTP Server Handler -gem 'gitlab-grack', '~> 1.0.0', require: 'grack' +gem 'gitlab-grack', '~> 1.0.1', require: 'grack' # LDAP Auth gem 'gitlab_omniauth-ldap', '1.0.2', require: "omniauth-ldap" @@ -107,6 +107,12 @@ gem 'tinder', '~> 1.9.2' # HipChat integration gem "hipchat", "~> 0.9.0" +# d3 +gem "d3_rails", "~> 3.1.4" + +# underscore-rails +gem "underscore-rails", "~> 1.4.4" + group :assets do gem "sass-rails" gem "coffee-rails" @@ -123,7 +129,7 @@ group :assets do gem "modernizr", "2.6.2" gem "raphael-rails", git: "https://github.com/gitlabhq/raphael-rails.git" gem 'bootstrap-sass' - gem "font-awesome-sass-rails", "~> 3.0.0" + gem "font-awesome-rails", "~> 3.1.1" gem "gemoji", "~> 1.2.1", require: 'emoji/railtie' gem "gon" end @@ -177,6 +183,7 @@ group :development, :test do gem 'poltergeist', '~> 1.3.0' gem 'spork', '~> 1.0rc' + gem 'jasmine' end group :test do diff --git a/Gemfile.lock b/Gemfile.lock index 0af3d516eec..b986539da1e 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -69,6 +69,8 @@ GEM celluloid (0.14.0) timers (>= 1.0.0) charlock_holmes (0.6.9.4) + childprocess (0.3.9) + ffi (~> 1.0, >= 1.0.11) chosen-rails (0.9.8) railties (~> 3.0) thor (~> 0.14) @@ -92,6 +94,8 @@ GEM simplecov (>= 0.7) thor crack (0.3.2) + d3_rails (3.1.4) + railties (>= 3.1.0) daemons (1.1.9) database_cleaner (1.0.1) debug_inspector (0.0.2) @@ -126,9 +130,8 @@ GEM eventmachine (>= 0.12.0) ffaker (1.16.0) ffi (1.8.1) - font-awesome-sass-rails (3.0.2.2) - railties (>= 3.1.1) - sass-rails (>= 3.1.1) + font-awesome-rails (3.1.1.3) + railties (>= 3.2, < 5.0) foreman (0.63.0) dotenv (>= 0.7) thor (>= 0.13.6) @@ -150,7 +153,7 @@ GEM pygments.rb (~> 0.4.2) sanitize (~> 2.0.3) stringex (~> 1.5.1) - gitlab-grack (1.0.0) + gitlab-grack (1.0.1) rack (~> 1.4.1) gitlab-grit (2.5.1) charlock_holmes (~> 0.6.9) @@ -160,7 +163,7 @@ GEM gitlab-pygments.rb (0.3.2) posix-spawn (~> 0.3.6) yajl-ruby (~> 1.1.0) - gitlab_git (1.2.1) + gitlab_git (1.3.0) activesupport (~> 3.2.13) github-linguist (~> 2.3.4) gitlab-grit (~> 2.5.1) @@ -216,6 +219,12 @@ GEM multi_xml (>= 0.5.2) httpauth (0.2.0) i18n (0.6.1) + jasmine (1.3.2) + jasmine-core (~> 1.3.1) + rack (~> 1.0) + rspec (>= 1.3.1) + selenium-webdriver (>= 0.1.3) + jasmine-core (1.3.1) journey (1.0.4) jquery-atwho-rails (0.3.0) jquery-rails (2.1.3) @@ -392,6 +401,7 @@ GEM rspec-mocks (~> 2.13.0) ruby-progressbar (1.0.2) rubyntlm (0.1.1) + rubyzip (0.9.9) sanitize (2.0.3) nokogiri (>= 1.4.4, < 1.6) sass (3.2.9) @@ -408,6 +418,11 @@ GEM select2-rails (3.3.1) sass-rails (>= 3.2) thor (~> 0.14) + selenium-webdriver (2.32.1) + childprocess (>= 0.2.5) + multi_json (~> 1.0) + rubyzip + websocket (~> 1.0.4) settingslogic (2.0.9) sexp_processor (4.2.1) shoulda-matchers (2.1.0) @@ -482,6 +497,7 @@ GEM uglifier (2.0.1) execjs (>= 0.3.0) multi_json (~> 1.0, >= 1.0.2) + underscore-rails (1.4.4) virtus (0.5.4) backports (~> 2.6.1) descendants_tracker (~> 0.0.1) @@ -490,6 +506,7 @@ GEM webmock (1.11.0) addressable (>= 2.2.7) crack (>= 0.3.2) + websocket (1.0.7) xpath (2.0.0) nokogiri (~> 1.3) yajl-ruby (1.1.0) @@ -510,21 +527,22 @@ DEPENDENCIES coffee-rails colored coveralls + d3_rails (~> 3.1.4) database_cleaner devise email_spec enumerize factory_girl_rails ffaker - font-awesome-sass-rails (~> 3.0.0) + font-awesome-rails (~> 3.1.1) foreman gemoji (~> 1.2.1) github-linguist github-markup (~> 0.7.4) gitlab-gollum-lib (~> 1.0.0) - gitlab-grack (~> 1.0.0) + gitlab-grack (~> 1.0.1) gitlab-pygments.rb (~> 0.3.2) - gitlab_git (~> 1.2.1) + gitlab_git (~> 1.3.0) gitlab_meta (= 5.0) gitlab_omniauth-ldap (= 1.0.2) gon @@ -536,6 +554,7 @@ DEPENDENCIES haml-rails hipchat (~> 0.9.0) httparty + jasmine jquery-atwho-rails (= 0.3.0) jquery-rails (= 2.1.3) jquery-turbolinks @@ -586,4 +605,5 @@ DEPENDENCIES tinder (~> 1.9.2) turbolinks uglifier + underscore-rails (~> 1.4.4) webmock @@ -1 +1 @@ -5.3.0.pre +5.3.0.beta1 diff --git a/app/assets/images/dark.png b/app/assets/images/dark-scheme-preview.png Binary files differindex 055a9069b63..055a9069b63 100644 --- a/app/assets/images/dark.png +++ b/app/assets/images/dark-scheme-preview.png diff --git a/app/assets/images/monokai.png b/app/assets/images/monokai-scheme-preview.png Binary files differindex 9477941778e..9477941778e 100644 --- a/app/assets/images/monokai.png +++ b/app/assets/images/monokai-scheme-preview.png diff --git a/app/assets/images/solarized_dark.png b/app/assets/images/solarized-dark-scheme-preview.png Binary files differindex 728964bc4c8..728964bc4c8 100644 --- a/app/assets/images/solarized_dark.png +++ b/app/assets/images/solarized-dark-scheme-preview.png diff --git a/app/assets/images/white.png b/app/assets/images/white-scheme-preview.png Binary files differindex 67eb8763044..67eb8763044 100644 --- a/app/assets/images/white.png +++ b/app/assets/images/white-scheme-preview.png diff --git a/app/assets/javascripts/admin.js.coffee b/app/assets/javascripts/admin.js.coffee index c83b74a76a2..da0077ea77b 100644 --- a/app/assets/javascripts/admin.js.coffee +++ b/app/assets/javascripts/admin.js.coffee @@ -19,11 +19,13 @@ class Admin modal = $('.change-owner-holder') - $('.change-owner-link').bind "click", -> + $('.change-owner-link').bind "click", (e) -> + e.preventDefault() $(this).hide() modal.show() - $('.change-owner-cancel-link').bind "click", -> + $('.change-owner-cancel-link').bind "click", (e) -> + e.preventDefault() modal.hide() $('.change-owner-link').show() diff --git a/app/assets/javascripts/application.js b/app/assets/javascripts/application.js index ab5fc1b860d..0767b82032d 100644 --- a/app/assets/javascripts/application.js +++ b/app/assets/javascripts/application.js @@ -27,3 +27,5 @@ //= require branch-graph //= require ace-src-noconflict/ace //= require_tree . +//= require d3 +//= require underscore diff --git a/app/assets/javascripts/commits.js.coffee b/app/assets/javascripts/commits.js.coffee index ab8eaa63228..de4c06a2728 100644 --- a/app/assets/javascripts/commits.js.coffee +++ b/app/assets/javascripts/commits.js.coffee @@ -42,6 +42,7 @@ class CommitsList @disable = true @initLoadMore: -> + $(document).unbind('scroll') $(document).endlessScroll bottomPixels: 400 fireDelay: 1000 diff --git a/app/assets/javascripts/stat_graph.js.coffee b/app/assets/javascripts/stat_graph.js.coffee new file mode 100644 index 00000000000..b129619696f --- /dev/null +++ b/app/assets/javascripts/stat_graph.js.coffee @@ -0,0 +1,6 @@ +class window.StatGraph + @log: {} + @get_log: -> + @log + @set_log: (data) -> + @log = data diff --git a/app/assets/javascripts/stat_graph_contributors.js.coffee b/app/assets/javascripts/stat_graph_contributors.js.coffee new file mode 100644 index 00000000000..12dfe4da841 --- /dev/null +++ b/app/assets/javascripts/stat_graph_contributors.js.coffee @@ -0,0 +1,61 @@ +class window.ContributorsStatGraph + init: (log) -> + @parsed_log = ContributorsStatGraphUtil.parse_log(log) + @set_current_field("commits") + total_commits = ContributorsStatGraphUtil.get_total_data(@parsed_log, @field) + author_commits = ContributorsStatGraphUtil.get_author_data(@parsed_log, @field) + @add_master_graph(total_commits) + @add_authors_graph(author_commits) + @change_date_header() + add_master_graph: (total_data) -> + @master_graph = new ContributorsMasterGraph(total_data) + @master_graph.draw() + add_authors_graph: (author_data) -> + @authors = [] + _.each(author_data, (d) => + author_header = @create_author_header(d) + $(".contributors-list").append(author_header) + @authors[d.author] = author_graph = new ContributorsAuthorGraph(d.dates) + author_graph.draw() + ) + format_author_commit_info: (author) -> + author.commits + " commits " + author.additions + " ++ / " + author.deletions + " --" + create_author_header: (author) -> + list_item = $('<li/>', { + class: 'person' + style: 'display: block;' + }) + author_name = $('<h4>' + author.author + '</h4>') + author_commit_info_span = $('<span/>', { + class: 'commits' + }) + author_commit_info = @format_author_commit_info(author) + author_commit_info_span.text(author_commit_info) + list_item.append(author_name) + list_item.append(author_commit_info_span) + list_item + redraw_master: -> + total_data = ContributorsStatGraphUtil.get_total_data(@parsed_log, @field) + @master_graph.set_data(total_data) + @master_graph.redraw() + redraw_authors: -> + $("ol").html("") + x_domain = ContributorsGraph.prototype.x_domain + author_commits = ContributorsStatGraphUtil.get_author_data(@parsed_log, @field, x_domain) + _.each(author_commits, (d) => + @redraw_author_commit_info(d) + $(@authors[d.author].list_item).appendTo("ol") + @authors[d.author].set_data(d.dates) + @authors[d.author].redraw() + ) + set_current_field: (field) -> + @field = field + change_date_header: -> + x_domain = ContributorsGraph.prototype.x_domain + print_date_format = d3.time.format("%B %e %Y"); + print = print_date_format(x_domain[0]) + " - " + print_date_format(x_domain[1]); + $("#date_header").text(print); + redraw_author_commit_info: (author) -> + author_list_item = $(@authors[author.author].list_item) + author_commit_info = @format_author_commit_info(author) + author_list_item.find("span").text(author_commit_info)
\ No newline at end of file diff --git a/app/assets/javascripts/stat_graph_contributors_graph.js.coffee b/app/assets/javascripts/stat_graph_contributors_graph.js.coffee new file mode 100644 index 00000000000..e7a120fb572 --- /dev/null +++ b/app/assets/javascripts/stat_graph_contributors_graph.js.coffee @@ -0,0 +1,166 @@ +class window.ContributorsGraph + MARGIN: + top: 20 + right: 20 + bottom: 30 + left: 50 + x_domain: null + y_domain: null + dates: [] + @set_x_domain: (data) => + @prototype.x_domain = data + @set_y_domain: (data) => + @prototype.y_domain = [0, d3.max(data, (d) -> + d.commits = d.commits ? d.additions ? d.deletions + )] + @init_x_domain: (data) => + @prototype.x_domain = d3.extent(data, (d) -> + d.date + ) + @init_y_domain: (data) => + @prototype.y_domain = [0, d3.max(data, (d) -> + d.commits = d.commits ? d.additions ? d.deletions + )] + @init_domain: (data) => + @init_x_domain(data) + @init_y_domain(data) + @set_dates: (data) => + @prototype.dates = data + set_x_domain: -> + @x.domain(@x_domain) + set_y_domain: -> + @y.domain(@y_domain) + set_domain: -> + @set_x_domain() + @set_y_domain() + create_scale: (width, height) -> + @x = d3.time.scale().range([0, width]).clamp(true) + @y = d3.scale.linear().range([height, 0]).nice() + draw_x_axis: -> + @svg.append("g").attr("class", "x axis").attr("transform", "translate(0, #{@height})") + .call(@x_axis); + draw_y_axis: -> + @svg.append("g").attr("class", "y axis").call(@y_axis) + set_data: (data) -> + @data = data + +class window.ContributorsMasterGraph extends ContributorsGraph + constructor: (@data) -> + @width = 1100 + @height = 125 + @x = null + @y = null + @x_axis = null + @y_axis = null + @area = null + @svg = null + @brush = null + @x_max_domain = null + process_dates: (data) -> + dates = @get_dates(data) + @parse_dates(data) + ContributorsGraph.set_dates(dates) + get_dates: (data) -> + _.pluck(data, 'date') + parse_dates: (data) -> + parseDate = d3.time.format("%Y-%m-%d").parse + data.forEach((d) -> + d.date = parseDate(d.date) + ) + create_scale: -> + super @width, @height + create_axes: -> + @x_axis = d3.svg.axis().scale(@x).orient("bottom") + @y_axis = d3.svg.axis().scale(@y).orient("left") + create_svg: -> + @svg = d3.select("#contributors-master").append("svg") + .attr("width", @width + @MARGIN.left + @MARGIN.right) + .attr("height", @height + @MARGIN.top + @MARGIN.bottom) + .attr("class", "tint-box") + .append("g") + .attr("transform", "translate(" + @MARGIN.left + "," + @MARGIN.top + ")") + create_area: (x, y) -> + @area = d3.svg.area().x((d) -> + x(d.date) + ).y0(@height).y1((d) -> + y(d.commits = d.commits ? d.additions ? d.deletions) + ).interpolate("basis") + create_brush: -> + @brush = d3.svg.brush().x(@x).on("brushend", @update_content); + draw_path: (data) -> + @svg.append("path").datum(data).attr("class", "area").attr("d", @area); + add_brush: -> + @svg.append("g").attr("class", "selection").call(@brush).selectAll("rect").attr("height", @height); + update_content: => + ContributorsGraph.set_x_domain(if @brush.empty() then @x_max_domain else @brush.extent()) + $("#brush_change").trigger('change') + draw: -> + @process_dates(@data) + @create_scale() + @create_axes() + ContributorsGraph.init_domain(@data) + @x_max_domain = @x_domain + @set_domain() + @create_area(@x, @y) + @create_svg() + @create_brush() + @draw_path(@data) + @draw_x_axis() + @draw_y_axis() + @add_brush() + redraw: -> + @process_dates(@data) + ContributorsGraph.set_y_domain(@data) + @set_y_domain() + @svg.select("path").datum(@data) + @svg.select("path").attr("d", @area) + @svg.select(".y.axis").call(@y_axis) + +class window.ContributorsAuthorGraph extends ContributorsGraph + constructor: (@data) -> + @width = 490 + @height = 130 + @x = null + @y = null + @x_axis = null + @y_axis = null + @area = null + @svg = null + @list_item = null + create_scale: -> + super @width, @height + create_axes: -> + @x_axis = d3.svg.axis().scale(@x).orient("bottom").tickFormat(d3.time.format("%m/%d")); + @y_axis = d3.svg.axis().scale(@y).orient("left") + create_area: (x, y) -> + @area = d3.svg.area().x((d) -> + parseDate = d3.time.format("%Y-%m-%d").parse + x(parseDate(d)) + ).y0(@height).y1((d) => + if @data[d]? then y(@data[d]) else y(0) + ).interpolate("basis") + create_svg: -> + @list_item = d3.selectAll(".person")[0].pop() + @svg = d3.select(@list_item).append("svg") + .attr("width", @width + @MARGIN.left + @MARGIN.right) + .attr("height", @height + @MARGIN.top + @MARGIN.bottom) + .attr("class", "spark") + .append("g") + .attr("transform", "translate(" + @MARGIN.left + "," + @MARGIN.top + ")") + draw_path: (data) -> + @svg.append("path").datum(data).attr("class", "area-contributor").attr("d", @area); + draw: -> + @create_scale() + @create_axes() + @set_domain() + @create_area(@x, @y) + @create_svg() + @draw_path(@dates) + @draw_x_axis() + @draw_y_axis() + redraw: -> + @set_domain() + @svg.select("path").datum(@dates) + @svg.select("path").attr("d", @area) + @svg.select(".x.axis").call(@x_axis) + @svg.select(".y.axis").call(@y_axis) diff --git a/app/assets/javascripts/stat_graph_contributors_util.js.coffee b/app/assets/javascripts/stat_graph_contributors_util.js.coffee new file mode 100644 index 00000000000..8f816313db3 --- /dev/null +++ b/app/assets/javascripts/stat_graph_contributors_util.js.coffee @@ -0,0 +1,91 @@ +window.ContributorsStatGraphUtil = + parse_log: (log) -> + total = {} + by_author = {} + for entry in log + @add_date(entry.date, total) unless total[entry.date]? + @add_author(entry.author, by_author) unless by_author[entry.author]? + @add_date(entry.date, by_author[entry.author]) unless by_author[entry.author][entry.date] + @store_data(entry, total[entry.date], by_author[entry.author][entry.date]) + total = _.toArray(total) + by_author = _.toArray(by_author) + total: total, by_author: by_author + + add_date: (date, collection) -> + collection[date] = {} + collection[date].date = date + + add_author: (author, by_author) -> + by_author[author] = {} + by_author[author].author = author + + store_data: (entry, total, by_author) -> + @store_commits(total, by_author) + @store_additions(entry, total, by_author) + @store_deletions(entry, total, by_author) + + store_commits: (total, by_author) -> + @add(total, "commits", 1) + @add(by_author, "commits", 1) + + add: (collection, field, value) -> + collection[field] ?= 0 + collection[field] += value + + store_additions: (entry, total, by_author) -> + entry.additions ?= 0 + @add(total, "additions", entry.additions) + @add(by_author, "additions", entry.additions) + + store_deletions: (entry, total, by_author) -> + entry.deletions ?= 0 + @add(total, "deletions", entry.deletions) + @add(by_author, "deletions", entry.deletions) + + get_total_data: (parsed_log, field) -> + log = parsed_log.total + total_data = @pick_field(log, field) + _.sortBy(total_data, (d) -> + d.date + ) + pick_field: (log, field) -> + total_data = [] + _.each(log, (d) -> + total_data.push(_.pick(d, [field, 'date'])) + ) + total_data + + get_author_data: (parsed_log, field, date_range = null) -> + log = parsed_log.by_author + author_data = [] + + _.each(log, (log_entry) => + parsed_log_entry = @parse_log_entry(log_entry, field, date_range) + if not _.isEmpty(parsed_log_entry.dates) + author_data.push(parsed_log_entry) + ) + + _.sortBy(author_data, (d) -> + d[field] + ).reverse() + + parse_log_entry: (log_entry, field, date_range) -> + parsed_entry = {} + parsed_entry.author = log_entry.author + parsed_entry.dates = {} + parsed_entry.commits = parsed_entry.additions = parsed_entry.deletions = 0 + _.each(_.omit(log_entry, 'author'), (value, key) => + if @in_range(value.date, date_range) + parsed_entry.dates[value.date] = value[field] + parsed_entry.commits += value.commits + parsed_entry.additions += value.additions + parsed_entry.deletions += value.deletions + ) + return parsed_entry + + in_range: (date, date_range) -> + if date_range is null || date_range[0] <= new Date(date) <= date_range[1] + true + else + false +
\ No newline at end of file diff --git a/app/assets/javascripts/users_select.js.coffee b/app/assets/javascripts/users_select.js.coffee index f9e523ea49f..8286ca2f0c1 100644 --- a/app/assets/javascripts/users_select.js.coffee +++ b/app/assets/javascripts/users_select.js.coffee @@ -14,23 +14,24 @@ $ -> userFormatSelection = (user) -> user.name - $('.ajax-users-select').select2 - placeholder: "Search for a user" - multiple: $('.ajax-users-select').hasClass('multiselect') - minimumInputLength: 0 - query: (query) -> - Api.users query.term, (users) -> - data = { results: users } - query.callback(data) + $('.ajax-users-select').each (i, select) -> + $(select).select2 + placeholder: "Search for a user" + multiple: $(select).hasClass('multiselect') + minimumInputLength: 0 + query: (query) -> + Api.users query.term, (users) -> + data = { results: users } + query.callback(data) - initSelection: (element, callback) -> - id = $(element).val() - if id isnt "" - Api.user(id, callback) + initSelection: (element, callback) -> + id = $(element).val() + if id isnt "" + Api.user(id, callback) - formatResult: userFormatResult - formatSelection: userFormatSelection - dropdownCssClass: "ajax-users-dropdown" - escapeMarkup: (m) -> # we do not want to escape markup since we are displaying html in results - m + formatResult: userFormatResult + formatSelection: userFormatSelection + dropdownCssClass: "ajax-users-dropdown" + escapeMarkup: (m) -> # we do not want to escape markup since we are displaying html in results + m diff --git a/app/assets/stylesheets/application.scss b/app/assets/stylesheets/application.scss index 85e43ed0d35..b1a23427add 100644 --- a/app/assets/stylesheets/application.scss +++ b/app/assets/stylesheets/application.scss @@ -37,6 +37,7 @@ @import "sections/wiki.scss"; @import "sections/wall.scss"; @import "sections/dashboard.scss"; +@import "sections/stat_graph.scss"; @import "highlight/white.scss"; @import "highlight/dark.scss"; diff --git a/app/assets/stylesheets/common.scss b/app/assets/stylesheets/common.scss index ccc6f7a9d2d..8c76b4baa22 100644 --- a/app/assets/stylesheets/common.scss +++ b/app/assets/stylesheets/common.scss @@ -418,3 +418,13 @@ img.emoji { overflow: hidden; height: 220px; } + +.navless-container { + margin-top: 30px; +} + +.description-block { + @extend .light-well; + @extend .light; + margin-bottom: 10px; +} diff --git a/app/assets/stylesheets/highlight/dark.scss b/app/assets/stylesheets/highlight/dark.scss index a56c98cc5f1..129d33dcac3 100644 --- a/app/assets/stylesheets/highlight/dark.scss +++ b/app/assets/stylesheets/highlight/dark.scss @@ -1,4 +1,4 @@ -.black .highlight { +.dark .highlight { background-color: #333; diff --git a/app/assets/stylesheets/sections/stat_graph.scss b/app/assets/stylesheets/sections/stat_graph.scss new file mode 100644 index 00000000000..4baec343d6e --- /dev/null +++ b/app/assets/stylesheets/sections/stat_graph.scss @@ -0,0 +1,48 @@ +.tint-box { + background: #f3f3f3; + position: relative; + margin-bottom: 10px; +} + +.area { + fill: #1db34f; + fill-opacity: 0.5; +} + +.axis { + fill: #aaa; + font-size: 10px; +} + +#contributors .person { + &:nth-child(even) { + float: right; + } + float: left; + margin-top: 10px; +} + +.contributors-list { + margin: 0 0 10px 0; + list-style: none; + padding: 0; +} + +#contributors .person .spark { + display: block; + background: #f3f3f3; +} + +#contributors .person .area-contributor { + fill: #f17f49; +} + +.selection rect { + fill: #333; + fill-opacity: 0.1; + stroke: #333; + stroke-width: 1px; + stroke-opacity: 0.4; + shape-rendering: crispedges; + stroke-dasharray: 3 3; +} diff --git a/app/assets/stylesheets/selects.scss b/app/assets/stylesheets/selects.scss index 7abbe80bd39..9b7b6ad583c 100644 --- a/app/assets/stylesheets/selects.scss +++ b/app/assets/stylesheets/selects.scss @@ -10,6 +10,10 @@ .ajax-users-select { width: 400px; + + &.input-large { + width: 210px; + } } .user-result { diff --git a/app/controllers/admin/groups_controller.rb b/app/controllers/admin/groups_controller.rb index df520bea773..c38461c89db 100644 --- a/app/controllers/admin/groups_controller.rb +++ b/app/controllers/admin/groups_controller.rb @@ -12,8 +12,6 @@ class Admin::GroupsController < Admin::ApplicationController @projects = @projects.not_in_group(@group) if @group.projects.present? @projects = @projects.all @projects.reject!(&:empty_repo?) - - @users = User.active end def new @@ -68,7 +66,8 @@ class Admin::GroupsController < Admin::ApplicationController end def project_teams_update - @group.add_users_to_project_teams(params[:user_ids], params[:project_access]) + @group.add_users_to_project_teams(params[:user_ids].split(','), params[:project_access]) + redirect_to [:admin, @group], notice: 'Users were successfully added.' end diff --git a/app/controllers/admin/projects_controller.rb b/app/controllers/admin/projects_controller.rb index bbb80cbb839..a63c4694a81 100644 --- a/app/controllers/admin/projects_controller.rb +++ b/app/controllers/admin/projects_controller.rb @@ -2,8 +2,10 @@ class Admin::ProjectsController < Admin::ApplicationController before_filter :project, only: [:edit, :show, :update, :destroy, :team_update] def index - @projects = Project.scoped - @projects = @projects.where(namespace_id: params[:namespace_id]) if params[:namespace_id].present? + 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 = @projects.with_push if params[:with_push].present? @projects = @projects.abandoned if params[:abandoned].present? @@ -14,9 +16,6 @@ class Admin::ProjectsController < Admin::ApplicationController def show @repository = @project.repository - @users = User.active - @users = @users.not_in_project(@project) if @project.users.present? - @users = @users.all end protected diff --git a/app/controllers/commit_controller.rb b/app/controllers/commit_controller.rb index 1329410891d..0f696ef9f5a 100644 --- a/app/controllers/commit_controller.rb +++ b/app/controllers/commit_controller.rb @@ -11,7 +11,11 @@ class CommitController < ProjectResourceController result = CommitLoadContext.new(project, current_user, params).execute @commit = result[:commit] - git_not_found! unless @commit + + if @commit.nil? + git_not_found! + return + end @suppress_diff = result[:suppress_diff] diff --git a/app/controllers/deploy_keys_controller.rb b/app/controllers/deploy_keys_controller.rb index c413c2fd1d9..35d28becd05 100644 --- a/app/controllers/deploy_keys_controller.rb +++ b/app/controllers/deploy_keys_controller.rb @@ -54,6 +54,6 @@ class DeployKeysController < ProjectResourceController protected def available_keys - @available_keys ||= DeployKey.in_projects(current_user.owned_projects).uniq + @available_keys ||= current_user.accessible_deploy_keys end end diff --git a/app/controllers/graphs_controller.rb b/app/controllers/graphs_controller.rb new file mode 100644 index 00000000000..5ae9c15c0f7 --- /dev/null +++ b/app/controllers/graphs_controller.rb @@ -0,0 +1,17 @@ +class GraphsController < ProjectResourceController + # Authorize + before_filter :authorize_read_project! + before_filter :authorize_code_access! + before_filter :require_non_empty_project + + def show + respond_to do |format| + format.html + format.js do + @repo = @project.repository + @stats = Gitlab::Git::GitStats.new(@repo.raw, @repo.root_ref) + @log = @stats.parsed_log.to_json + end + end + end +end diff --git a/app/controllers/groups_controller.rb b/app/controllers/groups_controller.rb index 8976262f4f7..e6559b8d8fe 100644 --- a/app/controllers/groups_controller.rb +++ b/app/controllers/groups_controller.rb @@ -1,7 +1,5 @@ class GroupsController < ApplicationController respond_to :html - layout 'group', except: [:new, :create] - before_filter :group, except: [:new, :create] # Authorize @@ -12,6 +10,10 @@ class GroupsController < ApplicationController # Load group projects before_filter :projects, except: [:new, :create] + layout :determine_layout + + before_filter :set_title, only: [:new, :create] + def new @group = Group.new end @@ -72,7 +74,7 @@ class GroupsController < ApplicationController end def team_members - @group.add_users_to_project_teams(params[:user_ids], params[:project_access]) + @group.add_users_to_project_teams(params[:user_ids].split(','), params[:project_access]) redirect_to people_group_path(@group), notice: 'Users were successfully added.' end @@ -134,4 +136,16 @@ class GroupsController < ApplicationController return render_404 end end + + def set_title + @title = 'New Group' + end + + def determine_layout + if [:new, :create].include?(action_name.to_sym) + 'navless' + else + 'group' + end + end end diff --git a/app/controllers/help_controller.rb b/app/controllers/help_controller.rb index b22280d2fd2..2958367e0f8 100644 --- a/app/controllers/help_controller.rb +++ b/app/controllers/help_controller.rb @@ -1,4 +1,15 @@ class HelpController < ApplicationController def index end + + def api + @category = params[:category] + @category = "README" if @category.blank? + + if File.exists?(Rails.root.join('doc', 'api', @category + '.md')) + render 'api' + else + not_found! + end + end end diff --git a/app/controllers/graph_controller.rb b/app/controllers/network_controller.rb index c79ed5ca3cc..3c8e747ba4e 100644 --- a/app/controllers/graph_controller.rb +++ b/app/controllers/network_controller.rb @@ -1,4 +1,4 @@ -class GraphController < ProjectResourceController +class NetworkController < ProjectResourceController include ExtractsPath include ApplicationHelper diff --git a/app/controllers/projects/application_controller.rb b/app/controllers/projects/application_controller.rb index 7e4776d2d75..86e4a7cbd6b 100644 --- a/app/controllers/projects/application_controller.rb +++ b/app/controllers/projects/application_controller.rb @@ -1,11 +1,4 @@ class Projects::ApplicationController < ApplicationController - - before_filter :authorize_admin_team_member! - - protected - - def user_team - @team ||= UserTeam.find_by_path(params[:id]) - end - + before_filter :project + before_filter :repository end diff --git a/app/controllers/projects/snippets_controller.rb b/app/controllers/projects/snippets_controller.rb new file mode 100644 index 00000000000..1165fa1c583 --- /dev/null +++ b/app/controllers/projects/snippets_controller.rb @@ -0,0 +1,91 @@ +class Projects::SnippetsController < Projects::ApplicationController + before_filter :module_enabled + before_filter :snippet, only: [:show, :edit, :destroy, :update, :raw] + + # Allow read any snippet + before_filter :authorize_read_project_snippet! + + # Allow write(create) snippet + before_filter :authorize_write_project_snippet!, only: [:new, :create] + + # Allow modify snippet + before_filter :authorize_modify_project_snippet!, only: [:edit, :update] + + # Allow destroy snippet + before_filter :authorize_admin_project_snippet!, only: [:destroy] + + layout 'project_resource' + + respond_to :html + + def index + @snippets = @project.snippets.fresh.non_expired + end + + def new + @snippet = @project.snippets.build + end + + def create + @snippet = @project.snippets.build(params[:project_snippet]) + @snippet.author = current_user + + if @snippet.save + redirect_to project_snippet_path(@project, @snippet) + else + respond_with(@snippet) + end + end + + def edit + end + + def update + if @snippet.update_attributes(params[:project_snippet]) + redirect_to project_snippet_path(@project, @snippet) + else + respond_with(@snippet) + end + end + + def show + @note = @project.notes.new(noteable: @snippet) + @target_type = :snippet + @target_id = @snippet.id + end + + def destroy + return access_denied! unless can?(current_user, :admin_project_snippet, @snippet) + + @snippet.destroy + + redirect_to project_snippets_path(@project) + end + + def raw + send_data( + @snippet.content, + type: "text/plain", + disposition: 'inline', + filename: @snippet.file_name + ) + end + + protected + + def snippet + @snippet ||= @project.snippets.find(params[:id]) + end + + def authorize_modify_project_snippet! + return render_404 unless can?(current_user, :modify_project_snippet, @snippet) + end + + def authorize_admin_project_snippet! + return render_404 unless can?(current_user, :admin_project_snippet, @snippet) + end + + def module_enabled + return render_404 unless @project.snippets_enabled + end +end diff --git a/app/controllers/projects/teams_controller.rb b/app/controllers/projects/teams_controller.rb index 17e7367364a..c7d51b84fc4 100644 --- a/app/controllers/projects/teams_controller.rb +++ b/app/controllers/projects/teams_controller.rb @@ -1,5 +1,7 @@ class Projects::TeamsController < Projects::ApplicationController + before_filter :authorize_admin_team_member! + def available @teams = current_user.is_admin? ? UserTeam.scoped : current_user.user_teams @teams = @teams.without_project(project) @@ -24,4 +26,9 @@ class Projects::TeamsController < Projects::ApplicationController redirect_to project_team_index_path(project) end + protected + + def user_team + @team ||= UserTeam.find_by_path(params[:id]) + end end diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb index e202ed3234e..fad681eeef8 100644 --- a/app/controllers/projects_controller.rb +++ b/app/controllers/projects_controller.rb @@ -7,7 +7,8 @@ class ProjectsController < ProjectResourceController before_filter :authorize_admin_project!, only: [:edit, :update, :destroy, :transfer] before_filter :require_non_empty_project, only: [:blob, :tree, :graph] - layout 'application', only: [:new, :create] + layout 'navless', only: [:new, :create, :fork] + before_filter :set_title, only: [:new, :create] def new @project = Project.new @@ -80,14 +81,15 @@ class ProjectsController < ProjectResourceController end def fork - @project = ::Projects::ForkContext.new(project, current_user).execute + @forked_project = ::Projects::ForkContext.new(project, current_user).execute respond_to do |format| format.html do - if @project.saved? && @project.forked? - redirect_to(@project, notice: 'Project was successfully forked.') + if @forked_project.saved? && @forked_project.forked? + redirect_to(@forked_project, notice: 'Project was successfully forked.') else - render action: "new" + @title = 'Fork project' + render action: "fork" end end format.js @@ -105,4 +107,10 @@ class ProjectsController < ProjectResourceController format.json { render :json => @suggestions } end end + + private + + def set_title + @title = 'New Project' + end end diff --git a/app/controllers/refs_controller.rb b/app/controllers/refs_controller.rb index e7def3984f8..cae9193a1da 100644 --- a/app/controllers/refs_controller.rb +++ b/app/controllers/refs_controller.rb @@ -14,7 +14,7 @@ class RefsController < ProjectResourceController elsif params[:destination] == "blob" project_blob_path(@project, (@id)) elsif params[:destination] == "graph" - project_graph_path(@project, @id, @options) + project_network_path(@project, @id, @options) else project_commits_path(@project, @id) end diff --git a/app/controllers/snippets_controller.rb b/app/controllers/snippets_controller.rb index a2e22a670a3..49b740af046 100644 --- a/app/controllers/snippets_controller.rb +++ b/app/controllers/snippets_controller.rb @@ -1,13 +1,6 @@ -class SnippetsController < ProjectResourceController - before_filter :module_enabled +class SnippetsController < ApplicationController before_filter :snippet, only: [:show, :edit, :destroy, :update, :raw] - # Allow read any snippet - before_filter :authorize_read_snippet! - - # Allow write(create) snippet - before_filter :authorize_write_snippet!, only: [:new, :create] - # Allow modify snippet before_filter :authorize_modify_snippet!, only: [:edit, :update] @@ -17,22 +10,47 @@ class SnippetsController < ProjectResourceController respond_to :html def index - @snippets = @project.snippets.fresh.non_expired + @snippets = Snippet.public.fresh.non_expired.page(params[:page]).per(20) + end + + def user_index + @user = User.find_by_username(params[:username]) + @snippets = @user.snippets.fresh.non_expired + + if @user == current_user + @snippets = case params[:scope] + when 'public' then + @snippets.public + when 'private' then + @snippets.private + else + @snippets + end + else + @snippets = @snippets.public + end + + @snippets = @snippets.page(params[:page]).per(20) + + if @user == current_user + render 'current_user_index' + else + render 'user_index' + end end def new - @snippet = @project.snippets.new + @snippet = PersonalSnippet.new end def create - @snippet = @project.snippets.new(params[:snippet]) + @snippet = PersonalSnippet.new(params[:personal_snippet]) @snippet.author = current_user - @snippet.save - if @snippet.valid? - redirect_to [@project, @snippet] + if @snippet.save + redirect_to snippet_path(@snippet) else - respond_with(@snippet) + respond_with @snippet end end @@ -40,27 +58,22 @@ class SnippetsController < ProjectResourceController end def update - @snippet.update_attributes(params[:snippet]) - - if @snippet.valid? - redirect_to [@project, @snippet] + if @snippet.update_attributes(params[:personal_snippet]) + redirect_to snippet_path(@snippet) else - respond_with(@snippet) + respond_with @snippet end end def show - @note = @project.notes.new(noteable: @snippet) - @target_type = :snippet - @target_id = @snippet.id end def destroy - return access_denied! unless can?(current_user, :admin_snippet, @snippet) + return access_denied! unless can?(current_user, :admin_personal_snippet, @snippet) @snippet.destroy - redirect_to project_snippets_path(@project) + redirect_to snippets_path end def raw @@ -75,18 +88,14 @@ class SnippetsController < ProjectResourceController protected def snippet - @snippet ||= @project.snippets.find(params[:id]) + @snippet ||= PersonalSnippet.where('author_id = :user_id or private is false', user_id: current_user.id).find(params[:id]) end def authorize_modify_snippet! - return render_404 unless can?(current_user, :modify_snippet, @snippet) + return render_404 unless can?(current_user, :modify_personal_snippet, @snippet) end def authorize_admin_snippet! - return render_404 unless can?(current_user, :admin_snippet, @snippet) - end - - def module_enabled - return render_404 unless @project.snippets_enabled + return render_404 unless can?(current_user, :admin_personal_snippet, @snippet) end end diff --git a/app/controllers/teams_controller.rb b/app/controllers/teams_controller.rb index 26c1d84f86b..57ab2a88e03 100644 --- a/app/controllers/teams_controller.rb +++ b/app/controllers/teams_controller.rb @@ -6,7 +6,9 @@ class TeamsController < ApplicationController before_filter :user_team, except: [:new, :create] - layout 'user_team', except: [:new, :create] + layout :determine_layout + + before_filter :set_title, only: [:new, :create] def show projects @@ -15,7 +17,7 @@ class TeamsController < ApplicationController def edit projects - @avaliable_projects = current_user.admin? ? Project.without_team(user_team) : current_user.owned_projects.without_team(user_team) + @avaliable_projects = current_user.owned_projects.without_team(user_team) end def update @@ -76,4 +78,16 @@ class TeamsController < ApplicationController def user_team @team ||= current_user.authorized_teams.find_by_path(params[:id]) end + + def set_title + @title = 'New Team' + end + + def determine_layout + if [:new, :create].include?(action_name.to_sym) + 'navless' + else + 'user_team' + end + end end diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb index e027057fe65..4947c33f959 100644 --- a/app/controllers/users_controller.rb +++ b/app/controllers/users_controller.rb @@ -1,7 +1,11 @@ class UsersController < ApplicationController + layout 'navless' + def show @user = User.find_by_username!(params[:username]) @projects = @user.authorized_projects.where('projects.id in (?)', current_user.authorized_projects.map(&:id)) @events = @user.recent_events.where(project_id: @projects.map(&:id)).limit(20) + + @title = @user.name end end diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index 4c0bbf813bd..663a414fff2 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -2,6 +2,24 @@ require 'digest/md5' require 'uri' module ApplicationHelper + COLOR_SCHEMES = { + 1 => 'white', + 2 => 'dark', + 3 => 'solarized-dark', + 4 => 'monokai', + } + COLOR_SCHEMES.default = 'white' + + # Helper method to access the COLOR_SCHEMES + # + # The keys are the `color_scheme_ids` + # The values are the `name` of the scheme. + # + # The preview images are `name-scheme-preview.png` + # The stylesheets should use the css class `.name` + def color_schemes + COLOR_SCHEMES.freeze + end # Check if a particular controller is the current one # @@ -124,17 +142,7 @@ module ApplicationHelper end def user_color_scheme_class - # in case we dont have current_user (ex. in mailer) - return 1 unless defined?(current_user) - - case current_user.color_scheme_id - when 1 then 'white' - when 2 then 'black' - when 3 then 'solarized-dark' - when 4 then 'monokai' - else - 'white' - end + COLOR_SCHEMES[current_user.try(:color_scheme_id)] end # Define whenever show last push event @@ -184,9 +192,12 @@ module ApplicationHelper alias_method :url_to_image, :image_url def users_select_tag(id, opts = {}) - css_class = "ajax-users-select" - css_class << " multiselect" if opts[:multiple] - hidden_field_tag(id, '', class: css_class) + css_class = "ajax-users-select " + css_class << "multiselect " if opts[:multiple] + css_class << (opts[:class] || '') + value = opts[:selected] || '' + + hidden_field_tag(id, value, class: css_class) end def body_data_page @@ -205,4 +216,13 @@ module ApplicationHelper def extra_config Gitlab.config.extra end + + def public_icon + content_tag :i, nil, class: 'icon-globe cblue' + end + + def private_icon + content_tag :i, nil, class: 'icon-lock cgreen' + end + end diff --git a/app/helpers/events_helper.rb b/app/helpers/events_helper.rb index 7155036eeed..7c1aee76d4a 100644 --- a/app/helpers/events_helper.rb +++ b/app/helpers/events_helper.rb @@ -83,4 +83,40 @@ module EventsHelper render "events/event_push", event: event end end + + def event_note_target_path(event) + if event.note? && event.note_commit? + project_commit_path(event.project, event.note_target) + else + url_for([event.project, event.note_target]) + end + end + + def event_note_title_html(event) + if event.note_target + if event.note_commit? + link_to project_commit_path(event.project, event.note_commit_id), class: "commit_short_id" do + "#{event.note_target_type} #{event.note_short_commit_id}" + 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 + end + else + link_to event_note_target_path(event) do + content_tag :strong do + "#{event.note_target_type} ##{truncate event.note_target_id}" + end + end + end + elsif event.wall_note? + link_to 'wall', project_wall_path(event.project) + else + content_tag :strong do + "(deleted)" + end + end + end end diff --git a/app/helpers/namespaces_helper.rb b/app/helpers/namespaces_helper.rb index a9a6c78654f..69ad3de5031 100644 --- a/app/helpers/namespaces_helper.rb +++ b/app/helpers/namespaces_helper.rb @@ -1,13 +1,7 @@ module NamespacesHelper def namespaces_options(selected = :current_user, scope = :default) - if current_user.admin - groups = Group.all - users = Namespace.root - else - groups = current_user.owned_groups.select {|n| n.type == 'Group'} - users = current_user.namespaces.reject {|n| n.type == 'Group'} - end - + groups = current_user.owned_groups.select {|n| n.type == 'Group'} + users = current_user.namespaces.reject {|n| n.type == 'Group'} global_opts = ["Global", [['/', Namespace.global_id]] ] group_opts = ["Groups", groups.sort_by(&:human_name).map {|g| [g.human_name, g.id]} ] diff --git a/app/helpers/snippets_helper.rb b/app/helpers/snippets_helper.rb index 6a91d616722..b0abc2cae33 100644 --- a/app/helpers/snippets_helper.rb +++ b/app/helpers/snippets_helper.rb @@ -8,4 +8,12 @@ module SnippetsHelper ] options_for_select(options) end + + def reliable_snippet_path(snippet) + if snippet.project_id? + project_snippet_path(snippet.project, snippet) + else + snippet_path(snippet) + end + end end diff --git a/app/helpers/tab_helper.rb b/app/helpers/tab_helper.rb index d2be4b1a7e6..19aba0f5f6d 100644 --- a/app/helpers/tab_helper.rb +++ b/app/helpers/tab_helper.rb @@ -73,7 +73,7 @@ module TabHelper end def project_tab_class - return "active" if current_page?(controller: "projects", action: :edit, id: @project) + return "active" if current_page?(controller: "/projects", action: :edit, id: @project) if ['services', 'hooks', 'deploy_keys', 'team_members'].include? controller.controller_name "active" diff --git a/app/helpers/tree_helper.rb b/app/helpers/tree_helper.rb index a8491dfe3ba..af633f6fd04 100644 --- a/app/helpers/tree_helper.rb +++ b/app/helpers/tree_helper.rb @@ -48,7 +48,7 @@ module TreeHelper end def plain_text_readme? filename - filename == 'README' + filename =~ /^README(.txt)?$/i end # Simple shortcut to File.join diff --git a/app/models/ability.rb b/app/models/ability.rb index 8f0a6141b75..0b6314cd555 100644 --- a/app/models/ability.rb +++ b/app/models/ability.rb @@ -7,7 +7,8 @@ class Ability when "Project" then project_abilities(user, subject) when "Issue" then issue_abilities(user, subject) when "Note" then note_abilities(user, subject) - when "Snippet" then snippet_abilities(user, subject) + when "ProjectSnippet" then project_snippet_abilities(user, subject) + when "PersonalSnippet" then personal_snippet_abilities(user, subject) when "MergeRequest" then merge_request_abilities(user, subject) when "Group", "Namespace" then group_abilities(user, subject) when "UserTeam" then user_team_abilities(user, subject) @@ -37,10 +38,14 @@ class Ability elsif team.reporters.include?(user) rules << project_report_rules - elsif team.guests.include?(user) or project.public? + elsif team.guests.include?(user) rules << project_guest_rules end + if project.public? + rules << public_project_rules + end + if project.owner == user || user.admin? rules << project_admin_rules end @@ -48,13 +53,30 @@ class Ability rules.flatten end + def public_project_rules + [ + :download_code, + :fork_project, + :read_project, + :read_wiki, + :read_issue, + :read_milestone, + :read_project_snippet, + :read_team_member, + :read_merge_request, + :read_note, + :write_issue, + :write_note + ] + end + def project_guest_rules [ :read_project, :read_wiki, :read_issue, :read_milestone, - :read_snippet, + :read_project_snippet, :read_team_member, :read_merge_request, :read_note, @@ -67,8 +89,8 @@ class Ability def project_report_rules project_guest_rules + [ :download_code, - :write_snippet, - :fork_project + :fork_project, + :write_project_snippet ] end @@ -84,11 +106,11 @@ class Ability project_dev_rules + [ :push_code_to_protected_branches, :modify_issue, - :modify_snippet, + :modify_project_snippet, :modify_merge_request, :admin_issue, :admin_milestone, - :admin_snippet, + :admin_project_snippet, :admin_team_member, :admin_merge_request, :admin_note, @@ -124,7 +146,7 @@ class Ability rules = [] # Only group owner and administrators can manage team - if team.owner == user || team.admin?(user) || user.admin? + if user.admin? || team.owner == user || team.admin?(user) rules << [ :manage_user_team ] end @@ -135,8 +157,7 @@ class Ability rules.flatten end - - [:issue, :note, :snippet, :merge_request].each do |name| + [:issue, :note, :project_snippet, :personal_snippet, :merge_request].each do |name| define_method "#{name}_abilities" do |user, subject| if subject.author == user [ diff --git a/app/models/event.rb b/app/models/event.rb index 97f96ac8f9e..793ed445947 100644 --- a/app/models/event.rb +++ b/app/models/event.rb @@ -75,7 +75,9 @@ class Event < ActiveRecord::Base end def target_title - target.try :title + if target && target.respond_to?(:title) + target.title + end end def push? @@ -241,6 +243,10 @@ class Event < ActiveRecord::Base target.noteable_type == "Commit" end + def note_project_snippet? + target.noteable_type == "Snippet" + end + def note_target target.noteable end diff --git a/app/models/network/graph.rb b/app/models/network/graph.rb index ffec4712e45..da815a85924 100644 --- a/app/models/network/graph.rb +++ b/app/models/network/graph.rb @@ -25,7 +25,7 @@ module Network def collect_notes h = Hash.new(0) @project.notes.where('noteable_type = ?' ,"Commit").group('notes.commit_id').select('notes.commit_id, count(notes.id) as note_count').each do |item| - h[item["commit_id"]] = item["note_count"] + h[item.commit_id] = item.note_count.to_i end h end diff --git a/app/models/note.rb b/app/models/note.rb index 7b7e6e99df4..9a3481faaaa 100644 --- a/app/models/note.rb +++ b/app/models/note.rb @@ -159,4 +159,10 @@ class Note < ActiveRecord::Base "wall" end end + + # FIXME: Hack for polymorphic associations with STI + # For more information wisit http://api.rubyonrails.org/classes/ActiveRecord/Associations/ClassMethods.html#label-Polymorphic+Associations + def noteable_type=(sType) + super(sType.to_s.classify.constantize.base_class.to_s) + end end diff --git a/app/models/personal_snippet.rb b/app/models/personal_snippet.rb new file mode 100644 index 00000000000..d581c6092aa --- /dev/null +++ b/app/models/personal_snippet.rb @@ -0,0 +1,18 @@ +# == Schema Information +# +# Table name: snippets +# +# id :integer not null, primary key +# title :string(255) +# content :text +# author_id :integer not null +# project_id :integer not null +# created_at :datetime not null +# updated_at :datetime not null +# file_name :string(255) +# expires_at :datetime +# type :string(255) +# private :boolean + +class PersonalSnippet < Snippet +end diff --git a/app/models/project.rb b/app/models/project.rb index 9147aed3d40..f5c2b4fe9af 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -57,7 +57,7 @@ class Project < ActiveRecord::Base has_many :milestones, dependent: :destroy has_many :users_projects, dependent: :destroy has_many :notes, dependent: :destroy - has_many :snippets, dependent: :destroy + has_many :snippets, dependent: :destroy, class_name: "ProjectSnippet" has_many :hooks, dependent: :destroy, class_name: "ProjectHook" has_many :protected_branches, dependent: :destroy has_many :user_team_project_relationships, dependent: :destroy @@ -110,11 +110,7 @@ class Project < ActiveRecord::Base class << self def abandoned - project_ids = Event.select('max(created_at) as latest_date, project_id'). - group('project_id'). - having('latest_date < ?', 6.months.ago).map(&:project_id) - - where(id: project_ids) + where('projects.last_activity_at < ?', 6.months.ago) end def with_push diff --git a/app/models/project_snippet.rb b/app/models/project_snippet.rb new file mode 100644 index 00000000000..a86f2e7a32f --- /dev/null +++ b/app/models/project_snippet.rb @@ -0,0 +1,27 @@ +# == Schema Information +# +# Table name: snippets +# +# id :integer not null, primary key +# title :string(255) +# content :text +# author_id :integer not null +# project_id :integer not null +# created_at :datetime not null +# updated_at :datetime not null +# file_name :string(255) +# expires_at :datetime +# type :string(255) +# private :boolean + +class ProjectSnippet < Snippet + belongs_to :project + belongs_to :author, class_name: "User" + + validates :project, presence: true + + # Scopes + scope :fresh, -> { order("created_at DESC") } + scope :non_expired, -> { where(["expires_at IS NULL OR expires_at > ?", Time.current]) } + scope :expired, -> { where(["expires_at IS NOT NULL AND expires_at < ?", Time.current]) } +end diff --git a/app/models/snippet.rb b/app/models/snippet.rb index c4ee35e0556..1b37ffe8339 100644 --- a/app/models/snippet.rb +++ b/app/models/snippet.rb @@ -11,29 +11,31 @@ # updated_at :datetime not null # file_name :string(255) # expires_at :datetime -# +# type :string(255) +# private :boolean class Snippet < ActiveRecord::Base include Linguist::BlobHelper - attr_accessible :title, :content, :file_name, :expires_at + attr_accessible :title, :content, :file_name, :expires_at, :private - belongs_to :project belongs_to :author, class_name: "User" + has_many :notes, as: :noteable, dependent: :destroy delegate :name, :email, to: :author, prefix: true, allow_nil: true validates :author, presence: true - validates :project, presence: true validates :title, presence: true, length: { within: 0..255 } validates :file_name, presence: true, length: { within: 0..255 } validates :content, presence: true # Scopes - scope :fresh, -> { order("created_at DESC") } - scope :non_expired, -> { where(["expires_at IS NULL OR expires_at > ?", Time.current]) } + scope :public, -> { where(private: false) } + scope :private, -> { where(private: true) } + scope :fresh, -> { order("created_at DESC") } scope :expired, -> { where(["expires_at IS NOT NULL AND expires_at < ?", Time.current]) } + scope :non_expired, -> { where(["expires_at IS NULL OR expires_at > ?", Time.current]) } def self.content_types [ diff --git a/app/models/user.rb b/app/models/user.rb index c6dd0868ba7..3f51d7a9938 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -78,6 +78,7 @@ class User < ActiveRecord::Base has_many :team_projects, through: :user_team_project_relationships # Projects + has_many :snippets, dependent: :destroy, foreign_key: :author_id, class_name: "Snippet" has_many :users_projects, dependent: :destroy has_many :issues, dependent: :destroy, foreign_key: :author_id has_many :notes, dependent: :destroy, foreign_key: :author_id @@ -89,6 +90,8 @@ class User < ActiveRecord::Base has_many :personal_projects, through: :namespace, source: :projects has_many :projects, through: :users_projects + has_many :master_projects, through: :users_projects, source: :project, + conditions: { users_projects: { project_access: UsersProject::MASTER } } has_many :own_projects, foreign_key: :creator_id, class_name: 'Project' has_many :owned_projects, through: :namespaces, source: :projects @@ -244,8 +247,12 @@ class User < ActiveRecord::Base end def authorized_teams - @team_ids ||= (user_teams.pluck(:id) + own_teams.pluck(:id)).uniq - UserTeam.where(id: @team_ids) + if admin? + UserTeam.scoped + else + @team_ids ||= (user_teams.pluck(:id) + own_teams.pluck(:id)).uniq + UserTeam.where(id: @team_ids) + end end # Team membership in authorized projects @@ -352,4 +359,8 @@ class User < ActiveRecord::Base def ldap_user? extern_uid && provider == 'ldap' end + + def accessible_deploy_keys + DeployKey.in_projects(self.master_projects).uniq + end end diff --git a/app/models/user_team.rb b/app/models/user_team.rb index 364ea0d7dd1..a036cedc4c7 100644 --- a/app/models/user_team.rb +++ b/app/models/user_team.rb @@ -111,6 +111,6 @@ class UserTeam < ActiveRecord::Base end def admin?(member) - user_team_user_relationships.with_user(member).first.group_admin? + user_team_user_relationships.with_user(member).first.try(:group_admin?) end end diff --git a/app/observers/project_observer.rb b/app/observers/project_observer.rb index dda7be625da..bd88bb838ef 100644 --- a/app/observers/project_observer.rb +++ b/app/observers/project_observer.rb @@ -15,6 +15,10 @@ class ProjectObserver < BaseObserver project.rename_repo if project.path_changed? end + def before_destroy(project) + project.repository.expire_cache unless project.empty_repo? + end + def after_destroy(project) GitlabShellWorker.perform_async( :remove_repository, diff --git a/app/observers/user_observer.rb b/app/observers/user_observer.rb index 6bb3c471d0c..e969405a598 100644 --- a/app/observers/user_observer.rb +++ b/app/observers/user_observer.rb @@ -10,12 +10,11 @@ class UserObserver < BaseObserver end def after_save user - if user.username_changed? - if user.namespace - user.namespace.update_attributes(path: user.username) - else - user.create_namespace!(path: user.username, name: user.username) - end + # Ensure user has namespace + user.create_namespace!(path: user.username, name: user.username) unless user.namespace + + if user.username_changed? || user.name_changed? + user.namespace.update_attributes(path: user.username, name: user.name) end end end diff --git a/app/views/admin/dashboard/index.html.haml b/app/views/admin/dashboard/index.html.haml index d3c938bb8f2..a69b26073f0 100644 --- a/app/views/admin/dashboard/index.html.haml +++ b/app/views/admin/dashboard/index.html.haml @@ -50,6 +50,14 @@ %h4 Stats %hr %p + Teams + %span.light.pull-right + = UserTeam.count + %p + Forks + %span.light.pull-right + = ForkedProjectLink.count + %p Issues %span.light.pull-right = Issue.count diff --git a/app/views/admin/groups/show.html.haml b/app/views/admin/groups/show.html.haml index 0e2e144d326..9c4b91b1bfa 100644 --- a/app/views/admin/groups/show.html.haml +++ b/app/views/admin/groups/show.html.haml @@ -1,120 +1,93 @@ %h3.page_title Group: #{@group.name} -%br -%table.zebra-striped - %thead - %tr - %th Group - %th - %tr - %td - %b - Name: - %td - = @group.name - - = link_to edit_admin_group_path(@group), class: "btn btn-small pull-right" do - %i.icon-edit - Edit - %tr - %td - %b - Description: - %td - = @group.description - %tr - %td - %b - Path: - %td - %span.monospace= File.join(Gitlab.config.gitlab_shell.repos_path, @group.path) - %tr - %td - %b - Owner: - %td - = @group.owner_name - .pull-right - = link_to "#", class: "btn btn-small change-owner-link" do - %i.icon-edit - Change owner + = link_to edit_admin_group_path(@group), class: "btn btn-small pull-right" do + %i.icon-edit + Edit +%hr +.row + .span6 + .ui-box + %h5.title + Group info: + %ul.well-list + %li + %span.light Name: + %strong= @group.name + %li + %span.light Path: + %strong + = @group.path - %tr.change-owner-holder.hide - %td.bgred - %b.cred - New Owner: - %td.bgred - = form_for [:admin, @group] do |f| - = f.select :owner_id, User.all.map { |user| [user.name, user.id] }, {}, {class: 'chosen'} - %div - = f.submit 'Change Owner', class: "btn btn-remove" - = link_to "Cancel", "#", class: "btn change-owner-cancel-link" + %li + %span.light Description: + %strong + = @group.description -- if @group.projects.any? - %fieldset - %legend Projects (#{@group.projects.count}) - %table - %thead - %tr - %th Project name - %th Path - %th Users - %th.cred Danger Zone! - - @group.projects.each do |project| - %tr - %td - = link_to project.name_with_namespace, [:admin, project] - %td - %span.monospace= project.path_with_namespace + ".git" - %td= project.users.count - %td.bgred - = link_to 'Transfer project to global namespace', remove_project_admin_group_path(@group, project_id: project.id), confirm: 'Remove project from group and move to global namespace. Are you sure?', method: :delete, class: "btn btn-remove small" + %li + %span.light Owned by: + %strong + - if @group.owner + = link_to @group.owner_name, admin_user_path(@group.owner) + - else + (deleted) + .pull-right + = link_to "#", class: "btn btn-small change-owner-link" do + %i.icon-edit + Change owner + %li.change-owner-holder.hide.bgred + .form-holder + %strong.cred New Owner: + = form_for [:admin, @group] do |f| + = users_select_tag(:"group[owner_id]") + .prepend-top-10 + = f.submit 'Change Owner', class: "btn btn-remove" + = link_to "Cancel", "#", class: "btn change-owner-cancel-link" - = form_tag project_teams_update_admin_group_path(@group), id: "new_team_member", class: "bulk_import", method: :put do - %table.zebra-striped - %thead - %tr - %th Users - %th Project Access: + %li + %span.light Created at: + %strong + = @group.created_at.stamp("March 1, 1999") - - @group.users.each do |user| - - next unless user - %tr{class: "user_#{user.id}"} - %td.name= link_to user.name, admin_user_path(user) - %td.projects_access - - user.authorized_projects.in_namespace(@group).each do |project| - - u_p = user.users_projects.in_project(project).first - - next unless u_p - %span - = project.name_with_namespace - = link_to "(#{ u_p.project_access_human })", edit_admin_project_member_path(project, user) - %tr - %td.input= select_tag :user_ids, options_from_collection_for_select(@users , :id, :name), multiple: true, data: {placeholder: 'Select users'}, class: 'chosen span5' - %td= select_tag :project_access, options_for_select(Project.access_options), {class: "project-access-select chosen span3"} - %tr - %td= submit_tag 'Add user to projects in group', class: "btn btn-create" - %td + .ui-box + %h5.title + Add user to Group projects: + .ui-box-body.form-holder + %p.light Read more about project permissions %strong= link_to "here", help_permissions_path, class: "vlink" -- else - %fieldset - %legend Group is empty + = form_tag project_teams_update_admin_group_path(@group), id: "new_team_member", class: "bulk_import", method: :put do + %div + = users_select_tag(:user_ids, multiple: true) + %div.prepend-top-10 + = select_tag :project_access, options_for_select(Project.access_options), {class: "project-access-select chosen span2"} + %hr + = submit_tag 'Add user to projects in group', class: "btn btn-create" + .ui-box + %h5.title + Users from Group projects + %small + (#{@group.users.count}) + %ul.well-list + - @group.users.sort_by(&:name).each do |user| + %li{class: dom_class(user)} + %strong + = link_to user.name, admin_user_path(user) + %span.pull-right.light + = pluralize user.authorized_projects.in_namespace(@group).count, 'project' -= form_tag project_update_admin_group_path(@group), class: "bulk_import", method: :put do - %fieldset - %legend Move projects to group - .alert - You can move only projects with existing repos - %br - Group projects will be moved in group directory and will not be accessible by old path - .clearfix - = label_tag :project_ids do + .span6 + .ui-box + %h5.title Projects - .input - = select_tag :project_ids, options_from_collection_for_select(@projects , :id, :name_with_namespace), multiple: true, data: {placeholder: 'Select projects'}, class: 'chosen span5' - .form-actions - = submit_tag 'Move projects', class: "btn btn-create" - + %small + (#{@group.projects.count}) + %ul.well-list + - @group.projects.sort_by(&:name).each do |project| + %li + %strong + = link_to project.name_with_namespace, [:admin, project] + %span.pull-right.light + %span.monospace= project.path_with_namespace + ".git" diff --git a/app/views/admin/projects/index.html.haml b/app/views/admin/projects/index.html.haml index 59831d83cc2..70ea460a06f 100644 --- a/app/views/admin/projects/index.html.haml +++ b/app/views/admin/projects/index.html.haml @@ -14,9 +14,9 @@ = text_field_tag :name, params[:name], class: "span2" .control-group - = label_tag :namespace_id, 'Namespace:', class: 'control-label' + = label_tag :owner_id, 'Owner:', class: 'control-label' .controls - = select_tag :namespace_id, namespaces_options(params[:namespace_id], :all), class: "chosen span2", prompt: "Any" + = users_select_tag :owner_id, selected: params[:owner_id], class: 'input-large' .control-group = label_tag :public_only, 'Public Only', class: 'control-label' .controls @@ -47,9 +47,9 @@ - @projects.each do |project| %li - if project.public - %i.icon-share + = public_icon - else - %i.icon-lock.cgreen + = private_icon = link_to project.name_with_namespace, [:admin, project] .pull-right = link_to 'Edit', edit_project_path(project), id: "edit_#{dom_id(project)}", class: "btn btn-small" diff --git a/app/views/admin/teams/index.html.haml b/app/views/admin/teams/index.html.haml index cf24ed5398f..05ddc3cee89 100644 --- a/app/views/admin/teams/index.html.haml +++ b/app/views/admin/teams/index.html.haml @@ -1,43 +1,48 @@ %h3.page_title - Teams + Teams (#{@teams.total_count}) %small allow you to organize groups of people that have a common focus. Use teams to simplify the process of assigning roles to groups of people. = link_to 'New Team', new_admin_team_path, class: "btn btn-small pull-right" - %br +%br = form_tag admin_teams_path, method: :get, class: 'form-inline' do - = text_field_tag :name, params[:name], class: "xlarge" + = text_field_tag :name, params[:name], class: "span6" = submit_tag "Search", class: "btn submit btn-primary" -%table - %thead - %tr - %th - Name - %i.icon-sort-down - %th Description - %th Path - %th Projects - %th Members - %th Owner - %th.cred Danger Zone! +%hr +%ul.bordered-list - @teams.each do |team| - %tr - %td - %strong= link_to team.name, admin_team_path(team) - %td= truncate team.description - %td= team.path - %td= team.projects.count - %td= team.members.count - %td - - if team.owner - = link_to team.owner.name, admin_user_path(team.owner) - - else - (deleted) - %td.bgred - = link_to 'Edit', edit_admin_team_path(team), id: "edit_#{dom_id(team)}", class: "btn btn-small" - = link_to 'Destroy', admin_team_path(team), confirm: "REMOVE #{team.name}? Are you sure?", method: :delete, class: "btn btn-small btn-remove" + %li + .clearfix + .pull-right.prepend-top-10 + = link_to 'Edit', edit_admin_team_path(team), id: "edit_#{dom_id(team)}", class: "btn btn-small" + = link_to 'Destroy', admin_team_path(team), confirm: "REMOVE #{team.name}? Are you sure?", method: :delete, class: "btn btn-small btn-remove" + + %h4 + = link_to admin_team_path(team) do + %i.icon-group + = team.name + + .clearfix.light.append-bottom-10 + %span + %b Owner: + - if team.owner + = link_to team.owner.name, admin_user_path(team.owner) + - else + (deleted) + \| + %span + %b Users: + %span.badge= team.members.count + \| + %span + %b Projects: + %span.badge= team.projects.count + + .clearfix + %p + = truncate team.description, length: 150 = paginate @teams, theme: "gitlab" diff --git a/app/views/admin/teams/projects/new.html.haml b/app/views/admin/teams/projects/new.html.haml index b60dad35214..dcb3dbbc433 100644 --- a/app/views/admin/teams/projects/new.html.haml +++ b/app/views/admin/teams/projects/new.html.haml @@ -1,23 +1,18 @@ %h3.page_title Team: #{@team.name} +%hr += form_tag admin_team_projects_path(@team), id: "assign_projects", class: "bulk_import", method: :post do + %h6 Choose Projects you want to assign: + .clearfix + = label_tag :project_ids, "Projects" + .input + = select_tag :project_ids, options_from_collection_for_select(@projects , :id, :name_with_namespace), multiple: true, data: {placeholder: 'Select projects'}, class: 'chosen span5' -%fieldset - %legend Projects (#{@team.projects.count}) - = form_tag admin_team_projects_path(@team), id: "assign_projects", class: "bulk_import", method: :post do - %table#projects_list - %thead - %tr - %th Project name - %th Max access - %th - - @team.projects.each do |project| - %tr.project - %td - = link_to project.name_with_namespace, [:admin, project] - %td - %span= @team.human_max_project_access(project) - %td - %tr - %td= select_tag :project_ids, options_from_collection_for_select(@projects , :id, :name_with_namespace), multiple: true, data: {placeholder: 'Select projects'}, class: 'chosen span5' - %td= select_tag :greatest_project_access, options_for_select(Project.access_options), {class: "project-access-select chosen span3" } - %td= submit_tag 'Add', class: "btn btn-primary", id: :assign_projects_to_team + %h6 Choose greatest user acces for your team in this projects: + .clearfix + = label_tag :greatest_project_access, "Greatest Access" + .input + = select_tag :greatest_project_access, options_for_select(Project.access_options), {class: "project-access-select chosen span3" } + + .form-actions + = submit_tag 'Add team to projects', class: "btn btn-create", id: :assign_projects_to_team diff --git a/app/views/admin/teams/show.html.haml b/app/views/admin/teams/show.html.haml index bd4d90b607f..cf105c096e3 100644 --- a/app/views/admin/teams/show.html.haml +++ b/app/views/admin/teams/show.html.haml @@ -1,93 +1,96 @@ %h3.page_title Team: #{@team.name} -%br -%table.zebra-striped - %thead - %tr - %th Team - %th - %tr - %td - %b - Name: - %td - = @team.name - - = link_to edit_admin_team_path(@team), class: "btn btn-small pull-right" do - %i.icon-edit - Edit - %tr - %td - %b - Description: - %td - = @team.description - %tr - %td - %b - Owner: - %td - = @team.owner.name - .pull-right - = link_to "#", class: "btn btn-small change-owner-link" do - %i.icon-edit - Change owner - %tr.change-owner-holder.hide - %td.bgred - %b.cred - New Owner: - %td.bgred - = form_for @team, url: admin_team_path(@team) do |f| - = f.select :owner_id, User.all.map { |user| [user.name, user.id] }, {}, {class: 'chosen'} - %div - = f.submit 'Change Owner', class: "btn btn-remove" - = link_to "Cancel", "#", class: "btn change-owner-cancel-link" + = link_to edit_admin_team_path(@team), class: "btn btn-small pull-right" do + %i.icon-edit + Edit +%hr -%fieldset - %legend - Members (#{@team.members.count}) - %span= link_to 'Add members', new_admin_team_member_path(@team), class: "btn btn-primary btn-small pull-right", id: :add_members_to_team - - if @team.members.any? - %table#members_list - %thead - %tr - %th User name - %th Default project access - %th Team access - %th.cred.span3 Danger Zone! - - @team.members.each do |member| - %tr.member{ class: "user_#{member.id}"} - %td + +.row + .span6 + .ui-box + %h5.title + Team info: + %ul.well-list + %li + %span.light Name: + %strong= @team.name + %li + %span.light Path: + %strong + = @team.path + + %li + %span.light Description: + %strong + = @team.description + + %li + %span.light Owned by: + %strong + - if @team.owner + = link_to @team.owner.name, admin_user_path(@team.owner) + - else + (deleted) + .pull-right + = link_to "#", class: "btn btn-small change-owner-link" do + %i.icon-edit + Change owner + %li.change-owner-holder.hide.bgred + .form-holder + %strong.cred New Owner: + = form_for @team, url: admin_team_path(@team) do |f| + = users_select_tag(:"user_team[owner_id]") + .prepend-top-10 + = f.submit 'Change Owner', class: "btn btn-remove" + = link_to "Cancel", "#", class: "btn change-owner-cancel-link" + + %li + %span.light Created at: + %strong + = @team.created_at.stamp("March 1, 1999") + + .span6 + .ui-box + %h5.title + Members (#{@team.members.count}) + .pull-right + = link_to 'Add members', new_admin_team_member_path(@team), class: "btn btn-small", id: :add_members_to_team + %ul.well-list#members_list + - @team.members.each do |member| + %li.member{ class: "user_#{member.id}"} = link_to [:admin, member] do - = member.name - %small= "(#{member.email})" - %td= @team.human_default_projects_access(member) - %td= @team.admin?(member) ? "Admin" : "Member" - %td.bgred - = link_to 'Edit', edit_admin_team_member_path(@team, member), class: "btn btn-small" - - = link_to 'Remove', admin_team_member_path(@team, member), confirm: 'Remove member from team. Are you sure?', method: :delete, class: "btn btn-remove btn-small", id: "remove_member_#{member.id}" + %strong + = member.name + .pull-right + %span.light + = @team.human_default_projects_access(member) + - if @team.admin?(member) + %span.label.label-info Admin + + = link_to 'Edit', edit_admin_team_member_path(@team, member), class: "btn btn-small" + + = link_to 'Remove', admin_team_member_path(@team, member), confirm: 'Remove member from team. Are you sure?', method: :delete, class: "btn btn-remove btn-small", id: "remove_member_#{member.id}" + + + .ui-box + %h5.title + Projects (#{@team.projects.count}) + .pull-right + = link_to 'Add projects', new_admin_team_project_path(@team), class: "btn btn-small", id: :assign_projects_to_team + %ul.well-list#projects_list + - @team.projects.each do |project| + %li.project + = link_to [:admin, project] do + %strong + = project.name_with_namespace -%fieldset - %legend - Projects (#{@team.projects.count}) - %span= link_to 'Add projects', new_admin_team_project_path(@team), class: "btn btn-primary btn-small pull-right", id: :assign_projects_to_team - - if @team.projects.any? - %table#projects_list - %thead - %tr - %th Project name - %th Max access - %th.cred.span3 Danger Zone! - - @team.projects.each do |project| - %tr.project - %td - = link_to project.name_with_namespace, [:admin, project] - %td - %span= @team.human_max_project_access(project) - %td.bgred - = link_to 'Edit', edit_admin_team_project_path(@team, project), class: "btn btn-small" - - = link_to 'Relegate', admin_team_project_path(@team, project), confirm: 'Remove project from team. Are you sure?', method: :delete, class: "btn btn-remove small", id: "relegate_project_#{project.id}" + .pull-right + %span.light + = @team.human_max_project_access(project) + + = link_to 'Edit', edit_admin_team_project_path(@team, project), class: "btn btn-small" + + = link_to 'Relegate', admin_team_project_path(@team, project), confirm: 'Remove project from team. Are you sure?', method: :delete, class: "btn btn-remove small", id: "relegate_project_#{project.id}" diff --git a/app/views/events/event/_note.html.haml b/app/views/events/event/_note.html.haml index 8bcfa95ff62..9dd0767e896 100644 --- a/app/views/events/event/_note.html.haml +++ b/app/views/events/event/_note.html.haml @@ -1,20 +1,6 @@ .event-title %span.author_name= link_to_author event - %span.event_label commented on - - if event.note_target - - if event.note_commit? - = event.note_target_type - = link_to event.note_short_commit_id, project_commit_path(event.project, event.note_commit_id), class: "commit_short_id" - - else - = link_to [event.project, event.note_target] do - %strong - #{event.note_target_type} ##{truncate event.note_target_id} - - - elsif event.wall_note? - = link_to 'wall', project_wall_path(event.project) - - else - %strong (deleted) - at + %span.event_label commented on #{event_note_title_html(event)} at - if event.project = link_to_project event.project - else diff --git a/app/views/graphs/show.html.haml b/app/views/graphs/show.html.haml new file mode 100644 index 00000000000..05bc1436e6d --- /dev/null +++ b/app/views/graphs/show.html.haml @@ -0,0 +1,31 @@ +.loading-graph + %center + .loading + %h3.page_title Building repository graph. Please wait a moment. + +.stat-graph + .header.clearfix + .pull-right + %select + %option{:value => "commits"} Commits + %option{:value => "additions"} Additions + %option{:value => "deletions"} Deletions + %h3#date_header.page_title + %input#brush_change{:type => "hidden"} + .graphs + #contributors-master + #contributors.clearfix + %ol.contributors-list.clearfix + +:javascript + $(".stat-graph").hide(); + + $.ajax({ + type: "GET", + url: location.href, + complete: function() { + $(".loading-graph").hide(); + $(".stat-graph").show(); + }, + dataType: "script" + }); diff --git a/app/views/graphs/show.js.haml b/app/views/graphs/show.js.haml new file mode 100644 index 00000000000..b7c9b4113e9 --- /dev/null +++ b/app/views/graphs/show.js.haml @@ -0,0 +1,16 @@ +:plain + controller = new ContributorsStatGraph + controller.init(#{@log}) + + $("select").change( function () { + var field = $(this).val() + controller.set_current_field(field) + controller.redraw_master() + controller.redraw_authors() + }) + + $("#brush_change").change( function () { + controller.change_date_header() + controller.redraw_authors() + }) + diff --git a/app/views/groups/_new_group_member.html.haml b/app/views/groups/_new_group_member.html.haml index 9cdbea60370..78db1c23320 100644 --- a/app/views/groups/_new_group_member.html.haml +++ b/app/views/groups/_new_group_member.html.haml @@ -5,7 +5,7 @@ %h6 1. Choose people you want in the team .clearfix = f.label :user_ids, "People" - .input= select_tag(:user_ids, options_from_collection_for_select(User.active.alphabetically, :id, :name), {data: {placeholder: "Select users"}, class: "chosen xxlarge", multiple: true}) + .input= users_select_tag(:user_ids, multiple: true) %h6 2. Set access level for them .clearfix diff --git a/app/views/groups/_new_member.html.haml b/app/views/groups/_new_member.html.haml index b3424b01bcb..4a762ed39ed 100644 --- a/app/views/groups/_new_member.html.haml +++ b/app/views/groups/_new_member.html.haml @@ -5,7 +5,7 @@ %h6 1. Choose people you want in the team .clearfix = f.label :user_ids, "People" - .input= select_tag(:user_ids, options_from_collection_for_select(User.not_in_project(@project).alphabetically, :id, :name), {data: {placeholder: "Select users"}, class: "chosen xxlarge", multiple: true}) + .input= users_select_tag(:user_ids, multiple: true) %h6 2. Set access level for them .clearfix diff --git a/app/views/groups/edit.html.haml b/app/views/groups/edit.html.haml index 0f8c140c067..aaedcc585c1 100644 --- a/app/views/groups/edit.html.haml +++ b/app/views/groups/edit.html.haml @@ -19,9 +19,9 @@ - @group.projects.each do |project| %li - if project.public - %i.icon-share + = public_icon - else - %i.icon-lock.cgreen + = private_icon = link_to project.name_with_namespace, project .pull-right = link_to 'Team', project_team_index_path(project), id: "edit_#{dom_id(project)}", class: "btn btn-small" diff --git a/app/views/groups/new.html.haml b/app/views/groups/new.html.haml index b395a8bc6a3..4aa58dedc4a 100644 --- a/app/views/groups/new.html.haml +++ b/app/views/groups/new.html.haml @@ -1,5 +1,3 @@ -%h3.page_title New Group -%hr = form_for @group do |f| - if @group.errors.any? .alert.alert-error @@ -15,14 +13,16 @@ .input = f.text_area :description, maxlength: 250, class: "xxlarge js-gfm-input", rows: 4 + .clearfix + .input + %ul + %li Group is kind of directory for several projects + %li All created groups are private + %li People within a group see only projects they have access to + %li All projects of group will be stored in a group directory + %li You will be able to move existing projects into group + .form-actions = f.submit 'Create group', class: "btn btn-create" - .padded - %ul - %li Group is kind of directory for several projects - %li All created groups are private - %li People within a group see only projects they have access to - %li All projects of group will be stored in a group directory - %li You will be able to move existing projects into group diff --git a/app/views/groups/show.atom.builder b/app/views/groups/show.atom.builder index 807ff5f39b4..edf03642d82 100644 --- a/app/views/groups/show.atom.builder +++ b/app/views/groups/show.atom.builder @@ -9,8 +9,8 @@ xml.feed "xmlns" => "http://www.w3.org/2005/Atom", "xmlns:media" => "http://sear @events.each do |event| if event.proper? xml.entry do - event_link = event.feed_url - event_title = event.feed_title + event_link = event_feed_url(event) + event_title = event_feed_title(event) xml.id "tag:#{request.host},#{event.created_at.strftime("%Y-%m-%d")}:#{event.id}" xml.link :href => event_link diff --git a/app/views/groups/show.html.haml b/app/views/groups/show.html.haml index 1ce008f7e85..8afc4ab4a12 100644 --- a/app/views/groups/show.html.haml +++ b/app/views/groups/show.html.haml @@ -13,7 +13,7 @@ .loading.hide .side.span4 - if @group.description.present? - .description.well.well-small.light + .description-block = @group.description = render "projects", projects: @projects .prepend-top-20 diff --git a/app/views/help/_api_layout.html.haml b/app/views/help/_api_layout.html.haml new file mode 100644 index 00000000000..2f649a509a5 --- /dev/null +++ b/app/views/help/_api_layout.html.haml @@ -0,0 +1,13 @@ +.row + .span3 + = link_to help_path, class: 'btn append-bottom-20 btn-small' do + %i.icon-angle-left + Back to help + %br + %ul.nav.nav-pills.nav-stacked + - %w(README projects project_snippets repositories deploy_keys users session issues milestones notes system_hooks).each do |file| + %li{class: file == @category ? 'active' : nil} + = link_to file.titleize, help_api_file_path(file) + + .span9.pull-right + = yield diff --git a/app/views/help/api.html.haml b/app/views/help/api.html.haml index 0c502ada8fa..b6ad5e14fc3 100644 --- a/app/views/help/api.html.haml +++ b/app/views/help/api.html.haml @@ -1,116 +1,15 @@ -= render layout: 'help/layout' do - %h3.page_title API += render layout: 'help/api_layout' do + %h3.page_title + %span.light API + %span + \/ + = @category.titleize %br - %ul.nav.nav-tabs.log-tabs.nav-small-tabs - %li.active - = link_to "README", "#README", 'data-toggle' => 'tab' - %li - = link_to "Projects", "#projects", 'data-toggle' => 'tab' - %li - = link_to "Snippets", "#snippets", 'data-toggle' => 'tab' - %li - = link_to "Repositories", "#repositories", 'data-toggle' => 'tab' - %li - = link_to "Users", "#users", 'data-toggle' => 'tab' - %li - = link_to "Session", "#session", 'data-toggle' => 'tab' - %li - = link_to "Issues", "#issues", 'data-toggle' => 'tab' - %li - = link_to "Milestones", "#milestones", 'data-toggle' => 'tab' - %li - = link_to "Notes", "#notes", 'data-toggle' => 'tab' - %li - = link_to "System Hooks", "#system_hooks", 'data-toggle' => 'tab' - - .tab-content - .tab-pane.active#README - .file_holder - .file_title - %i.icon-file - README - .file_content.wiki - = preserve do - = markdown File.read(Rails.root.join("doc", "api", "README.md")) - - .tab-pane#projects - .file_holder - .file_title - %i.icon-file - Projects - .file_content.wiki - = preserve do - = markdown File.read(Rails.root.join("doc", "api", "projects.md")) - - .tab-pane#snippets - .file_holder - .file_title - %i.icon-file - Projects Snippets - .file_content.wiki - = preserve do - = markdown File.read(Rails.root.join("doc", "api", "snippets.md")) - - .tab-pane#repositories - .file_holder - .file_title - %i.icon-file - Projects - .file_content.wiki - = preserve do - = markdown File.read(Rails.root.join("doc", "api", "repositories.md")) - - .tab-pane#users - .file_holder - .file_title - %i.icon-file - Users - .file_content.wiki - = preserve do - = markdown File.read(Rails.root.join("doc", "api", "users.md")) - - .tab-pane#session - .file_holder - .file_title - %i.icon-file - Session - .file_content.wiki - = preserve do - = markdown File.read(Rails.root.join("doc", "api", "session.md")) - - .tab-pane#issues - .file_holder - .file_title - %i.icon-file - Issues - .file_content.wiki - = preserve do - = markdown File.read(Rails.root.join("doc", "api", "issues.md")) - - .tab-pane#milestones - .file_holder - .file_title - %i.icon-file - Milestones - .file_content.wiki - = preserve do - = markdown File.read(Rails.root.join("doc", "api", "milestones.md")) - - .tab-pane#notes - .file_holder - .file_title - %i.icon-file - Notes - .file_content.wiki - = preserve do - = markdown File.read(Rails.root.join("doc", "api", "notes.md")) - - .tab-pane#system_hooks - .file_holder - .file_title - %i.icon-file - System Hooks - .file_content.wiki - = preserve do - = markdown File.read(Rails.root.join("doc", "api", "system_hooks.md")) + .file_holder + .file_title + %i.icon-file + = @category + .file_content.wiki + = preserve do + = markdown File.read(Rails.root.join("doc", "api", "#{@category}.md")) diff --git a/app/views/help/permissions.html.haml b/app/views/help/permissions.html.haml index 2075753e82a..4748b296960 100644 --- a/app/views/help/permissions.html.haml +++ b/app/views/help/permissions.html.haml @@ -54,7 +54,6 @@ %li Add new team members %li Push to protected branches %li Remove protected branches - %li Push with force option %li Edit project %li Add Deploy Keys to project %li Configure Project Hooks @@ -62,5 +61,6 @@ %fieldset %legend Owner %ul + %li Switch public mode %li Transfer project to another namespace %li Remove project diff --git a/app/views/issues/_form.html.haml b/app/views/issues/_form.html.haml index 2bb5e6ca5ac..57460e623ab 100644 --- a/app/views/issues/_form.html.haml +++ b/app/views/issues/_form.html.haml @@ -20,8 +20,11 @@ %i.icon-user Assign to .input - = f.select(:assignee_id, @project.users.alphabetically.collect {|p| [ p.name, p.id ] }, { include_blank: "Select a user" }, {class: 'chosen'}) - = link_to 'Assign to me', '#', class: 'btn btn-small assign-to-me-link' + .pull-left + = f.select(:assignee_id, @project.users.alphabetically.collect {|p| [ p.name, p.id ] }, { include_blank: "Select a user" }, {class: 'chosen'}) + .pull-right + + = link_to 'Assign to me', '#', class: 'btn btn-small assign-to-me-link' .issue_milestone.pull-left = f.label :milestone_id do %i.icon-time @@ -64,6 +67,9 @@ event.preventDefault(); } }) + .bind( "click", function( event ) { + $( this ).autocomplete("search", ""); + }) .autocomplete({ minLength: 0, source: function( request, response ) { diff --git a/app/views/layouts/_head_panel.html.haml b/app/views/layouts/_head_panel.html.haml index 2ea6c3e46d9..f0b001f6efc 100644 --- a/app/views/layouts/_head_panel.html.haml +++ b/app/views/layouts/_head_panel.html.haml @@ -18,6 +18,9 @@ %li = link_to public_root_path, title: "Public area", class: 'has_bottom_tooltip', 'data-original-title' => 'Public area' do %i.icon-globe + %li + = link_to user_snippets_path(current_user), title: "My snippets", class: 'has_bottom_tooltip', 'data-original-title' => 'Public area' do + %i.icon-paste - if current_user.is_admin? %li = link_to admin_root_path, title: "Admin area", class: 'has_bottom_tooltip', 'data-original-title' => 'Admin area' do diff --git a/app/views/layouts/nav/_dashboard.html.haml b/app/views/layouts/nav/_dashboard.html.haml index 2ac35050b64..cae24c3170d 100644 --- a/app/views/layouts/nav/_dashboard.html.haml +++ b/app/views/layouts/nav/_dashboard.html.haml @@ -13,8 +13,6 @@ = link_to merge_requests_dashboard_path do Merge Requests %span.count= current_user.cared_merge_requests.opened.count - = nav_link(path: 'search#show') do - = link_to "Search", search_path = nav_link(controller: :help) do = link_to "Help", help_path diff --git a/app/views/layouts/nav/_project.html.haml b/app/views/layouts/nav/_project.html.haml index ec3da964037..1c5781daab0 100644 --- a/app/views/layouts/nav/_project.html.haml +++ b/app/views/layouts/nav/_project.html.haml @@ -9,8 +9,10 @@ = link_to 'Files', project_tree_path(@project, @ref || @repository.root_ref) = nav_link(controller: %w(commit commits compare repositories protected_branches)) do = link_to "Commits", project_commits_path(@project, @ref || @repository.root_ref) - = nav_link(controller: %w(graph)) do - = link_to "Network", project_graph_path(@project, @ref || @repository.root_ref) + = nav_link(controller: %w(network)) do + = link_to "Network", project_network_path(@project, @ref || @repository.root_ref) + = nav_link(controller: %w(graphs)) do + = link_to "Graphs", project_graph_path(@project, @ref || @repository.root_ref) - if @project.issues_enabled = nav_link(controller: %w(issues milestones labels)) do diff --git a/app/views/layouts/navless.html.haml b/app/views/layouts/navless.html.haml new file mode 100644 index 00000000000..7325664bc19 --- /dev/null +++ b/app/views/layouts/navless.html.haml @@ -0,0 +1,10 @@ +!!! 5 +%html{ lang: "en"} + = render "layouts/head", title: @title + %body{class: "#{app_theme} application", :'data-page' => body_data_page} + = render "layouts/head_panel", title: @title + = render "layouts/flash" + + .container.navless-container + .content + = yield diff --git a/app/views/layouts/public.html.haml b/app/views/layouts/public.html.haml index 435250b6825..e330da9bd73 100644 --- a/app/views/layouts/public.html.haml +++ b/app/views/layouts/public.html.haml @@ -1,17 +1,20 @@ !!! 5 %html{ lang: "en"} = render "layouts/head", title: "Public Projects" - %body{class: "#{app_theme} application"} - %header.navbar.navbar-static-top.navbar-gitlab - .navbar-inner - .container - %div.app_logo - %span.separator - = link_to root_path, class: "home" do - %h1 GITLAB - %span.separator - %h1.project_name Public Projects - .container + %body{class: "#{app_theme} application", :'data-page' => body_data_page} + - if current_user + = render "layouts/head_panel", title: "Public Projects" + - else + %header.navbar.navbar-static-top.navbar-gitlab + .navbar-inner + .container + %div.app_logo + %span.separator + = link_to root_path, class: "home" do + %h1 GITLAB + %span.separator + %h1.project_name Public Projects + + .container.navless-container .content - .prepend-top-20 - = yield + = yield diff --git a/app/views/layouts/search.html.haml b/app/views/layouts/search.html.haml new file mode 100644 index 00000000000..177b3a4f8f4 --- /dev/null +++ b/app/views/layouts/search.html.haml @@ -0,0 +1,10 @@ +!!! 5 +%html{ lang: "en"} + = render "layouts/head", title: "Search" + %body{class: "#{app_theme} application", :'data-page' => body_data_page} + = render "layouts/head_panel", title: "Search" + = render "layouts/flash" + + .container.navless-container + .content + = yield diff --git a/app/views/layouts/snippets.html.haml b/app/views/layouts/snippets.html.haml new file mode 100644 index 00000000000..e98aba445af --- /dev/null +++ b/app/views/layouts/snippets.html.haml @@ -0,0 +1,20 @@ +!!! 5 +%html{ lang: "en"} + = render "layouts/head", title: "Snipepts" + %body{class: "#{app_theme} application", :'data-page' => body_data_page} + = render "layouts/head_panel", title: "Snippets" + = render "layouts/flash" + %nav.main-nav + .container + %ul + = nav_link(path: 'snippets#user_index', html_options: {class: 'home'}) do + = link_to user_snippets_path(current_user), title: "My Snippets" do + %i.icon-home + = nav_link(path: 'snippets#new') do + = link_to new_snippet_path do + New snippet + = nav_link(path: 'snippets#index') do + = link_to snippets_path do + Discover snippets + .container + .content= yield diff --git a/app/views/graph/_head.html.haml b/app/views/network/_head.html.haml index 7a5b3c6f43d..62ab8b049ac 100644 --- a/app/views/graph/_head.html.haml +++ b/app/views/network/_head.html.haml @@ -5,7 +5,7 @@ .pull-left = render partial: 'shared/ref_switcher', locals: {destination: 'graph'} .pull-left - = form_tag project_graph_path(@project, @id), method: :get do |f| + = form_tag project_network_path(@project, @id), method: :get do |f| .control-group = label_tag :filter_ref, "Show only selected ref", class: 'control-label light' .controls @@ -14,7 +14,7 @@ = hidden_field_tag(key, value, id: nil) unless key == "filter_ref" .search.pull-right - = form_tag project_graph_path(@project, @id), method: :get do |f| + = form_tag project_network_path(@project, @id), method: :get do |f| .control-group = label_tag :search , "Looking for commit:", class: 'control-label light' .controls diff --git a/app/views/graph/show.html.haml b/app/views/network/show.html.haml index 0ee6648317c..a480ceaf995 100644 --- a/app/views/graph/show.html.haml +++ b/app/views/network/show.html.haml @@ -11,7 +11,7 @@ $(this).closest('form').submit(); }); branch_graph = new BranchGraph($("#holder"), { - url: '#{project_graph_path(@project, @ref, @options.merge(format: :json))}', + url: '#{project_network_path(@project, @ref, @options.merge(format: :json))}', commit_url: '#{project_commit_path(@project, 'ae45ca32').gsub("ae45ca32", "%s")}', ref: '#{@ref}', commit_id: '#{@commit.id}' diff --git a/app/views/graph/show.json.erb b/app/views/network/show.json.erb index 9a62cdb3dc9..9a62cdb3dc9 100644 --- a/app/views/graph/show.json.erb +++ b/app/views/network/show.json.erb diff --git a/app/views/profiles/design.html.haml b/app/views/profiles/design.html.haml index 878297fe49d..9ada69c9d5d 100644 --- a/app/views/profiles/design.html.haml +++ b/app/views/profiles/design.html.haml @@ -40,23 +40,9 @@ %i.icon-ok Saved .code_highlight_opts - = label_tag do - .prev - = image_tag "white.png" - = f.radio_button :color_scheme_id, 1 - White - = label_tag do - .prev - = image_tag "dark.png" - = f.radio_button :color_scheme_id, 2 - Dark - = label_tag do - .prev - = image_tag "solarized_dark.png" - = f.radio_button :color_scheme_id, 3 - Solarized Dark - = label_tag do - .prev - = image_tag "monokai.png" - = f.radio_button :color_scheme_id, 4 - Monokai + - color_schemes.each do |color_scheme_id, color_scheme| + = label_tag do + .prev + = image_tag "#{color_scheme}-scheme-preview.png" + = f.radio_button :color_scheme_id, color_scheme_id + = color_scheme.gsub(/[-_]+/, ' ').humanize diff --git a/app/views/projects/_clone_panel.html.haml b/app/views/projects/_clone_panel.html.haml index f8276c3c2b6..ebce06edf74 100644 --- a/app/views/projects/_clone_panel.html.haml +++ b/app/views/projects/_clone_panel.html.haml @@ -7,11 +7,12 @@ - unless @project.empty_repo? - if can?(current_user, :fork_project, @project) && @project.namespace != current_user.namespace - if current_user.already_forked?(@project) - = link_to project_path(current_user.fork_of(@project)), class: 'btn grouped btn-primary' do + = link_to project_path(current_user.fork_of(@project)), class: 'btn grouped disabled' do + %i.icon-code-fork Forked - else = link_to fork_project_path(@project), title: "Fork", class: "btn grouped", method: "POST" do - %i.icon-copy + %i.icon-code-fork Fork - if can? current_user, :download_code, @project = link_to archive_project_repository_path(@project), class: "btn grouped" do diff --git a/app/views/projects/fork.html.haml b/app/views/projects/fork.html.haml new file mode 100644 index 00000000000..a1c109e5d62 --- /dev/null +++ b/app/views/projects/fork.html.haml @@ -0,0 +1,19 @@ +.alert.alert-error.alert-block + %h4 + %i.icon-code-fork + Fork Error! + %p + You are trying to fork + = link_to_project @project + but it fails due to next reason: + + + - if @forked_project && @forked_project.errors.any? + %p + – + = @forked_project.errors.full_messages.first + + %p + = link_to fork_project_path(@project), title: "Fork", class: "btn", method: "POST" do + %i.icon-code-fork + Try to Fork again diff --git a/app/views/projects/new.html.haml b/app/views/projects/new.html.haml index e9099f264bc..0754ee83114 100644 --- a/app/views/projects/new.html.haml +++ b/app/views/projects/new.html.haml @@ -1,6 +1,4 @@ .project-edit-container - %h3.page_title New Project - %hr .project-edit-errors = render 'projects/errors' .project-edit-content diff --git a/app/views/projects/show.html.haml b/app/views/projects/show.html.haml index b2e9fd8431a..7d708ce7b8f 100644 --- a/app/views/projects/show.html.haml +++ b/app/views/projects/show.html.haml @@ -3,7 +3,7 @@ .row .span9 = render "events/event_last_push", event: @last_push - .content_list= render @events + .content_list .loading.hide .span3 .light-well @@ -42,6 +42,7 @@ %p Owner: #{link_to @project.owner_name, @project.owner} - if @project.forked_from_project %p + %i.icon-code-fork Forked from: = link_to @project.forked_from_project.name_with_namespace, project_path(@project.forked_from_project) diff --git a/app/views/projects/snippets/_blob.html.haml b/app/views/projects/snippets/_blob.html.haml new file mode 100644 index 00000000000..e0d1669acbe --- /dev/null +++ b/app/views/projects/snippets/_blob.html.haml @@ -0,0 +1,15 @@ +.file_holder + .file_title + %i.icon-file + %strong= @snippet.file_name + %span.options + .btn-group.tree-btn-group.pull-right + - if can?(current_user, :admin_project_snippet, @project) || @snippet.author == current_user + = link_to "Edit", edit_project_snippet_path(@project, @snippet), class: "btn btn-tiny", title: 'Edit Snippet' + = link_to "raw", raw_project_snippet_path(@project, @snippet), class: "btn btn-tiny", target: "_blank" + .file_content.code + - unless @snippet.content.empty? + %div{class: user_color_scheme_class} + = raw @snippet.colorize(formatter: :gitlab) + - else + %p.nothing_here_message Empty file diff --git a/app/views/projects/snippets/_form.html.haml b/app/views/projects/snippets/_form.html.haml new file mode 100644 index 00000000000..99a8761daef --- /dev/null +++ b/app/views/projects/snippets/_form.html.haml @@ -0,0 +1,41 @@ +%h3.page_title + = @snippet.new_record? ? "New Snippet" : "Edit Snippet ##{@snippet.id}" +%hr +.snippet-form-holder + = form_for [@project, @snippet], as: :project_snippet, url: url do |f| + -if @snippet.errors.any? + .alert.alert-error + %ul + - @snippet.errors.full_messages.each do |msg| + %li= msg + + .clearfix + = f.label :title + .input= f.text_field :title, placeholder: "Example Snippet", class: 'input-xlarge', required: true + .clearfix + = f.label "Lifetime" + .input= f.select :expires_at, lifetime_select_options, {}, {class: 'chosen span2'} + .clearfix + .file-editor + = f.label :file_name, "File" + .input + .file_holder.snippet + .file_title + = f.text_field :file_name, placeholder: "example.rb", class: 'snippet-file-name', required: true + .file_content.code + %pre#editor= @snippet.content + = f.hidden_field :content, class: 'snippet-file-content' + + .form-actions + = f.submit 'Save', class: "btn-save btn" + = link_to "Cancel", project_snippets_path(@project), class: " btn" + - unless @snippet.new_record? + .pull-right= link_to 'Destroy', project_snippet_path(@project, @snippet), confirm: 'Are you sure?', method: :delete, class: "btn pull-right danger delete-snippet", id: "destroy_snippet_#{@snippet.id}" + + +:javascript + var editor = ace.edit("editor"); + $(".snippet-form-holder form").submit(function(){ + $(".snippet-file-content").val(editor.getValue()); + }); + diff --git a/app/views/projects/snippets/_snippet.html.haml b/app/views/projects/snippets/_snippet.html.haml new file mode 100644 index 00000000000..72865bf2872 --- /dev/null +++ b/app/views/projects/snippets/_snippet.html.haml @@ -0,0 +1,25 @@ +%li + .snippet-title + - if snippet.private? + %i.icon-lock.cgreen + - else + %i.icon-globe.cblue + = link_to reliable_snippet_path(snippet) do + %h5.inline + = truncate(snippet.title, length: 60) + %span.cgray + = snippet.file_name + + %small.pull-right.cgray + Expires: + - if snippet.expires_at + = snippet.expires_at.to_date.to_s(:short) + - else + Never + + .snippet-info.prepend-left-20 + = "##{snippet.id}" + %span.light + by + = image_tag gravatar_icon(snippet.author_email), class: "avatar avatar-inline s16" + = snippet.author_name diff --git a/app/views/projects/snippets/edit.html.haml b/app/views/projects/snippets/edit.html.haml new file mode 100644 index 00000000000..e28b7d4937e --- /dev/null +++ b/app/views/projects/snippets/edit.html.haml @@ -0,0 +1 @@ += render "projects/snippets/form", url: project_snippet_path(@project, @snippet) diff --git a/app/views/projects/snippets/index.html.haml b/app/views/projects/snippets/index.html.haml new file mode 100644 index 00000000000..ce36bed1e0d --- /dev/null +++ b/app/views/projects/snippets/index.html.haml @@ -0,0 +1,13 @@ +%h3.page_title + Snippets + %small share code pastes with others out of git repository + + - if can? current_user, :write_project_snippet, @project + = link_to new_project_snippet_path(@project), class: "btn btn-small add_new pull-right", title: "New Snippet" do + Add new snippet +%hr +%ul.bordered-list + = render partial: "projects/snippets/snippet", collection: @snippets + - if @snippets.empty? + %li + %h3.nothing_here_message Nothing here. diff --git a/app/views/projects/snippets/new.html.haml b/app/views/projects/snippets/new.html.haml new file mode 100644 index 00000000000..460af34f676 --- /dev/null +++ b/app/views/projects/snippets/new.html.haml @@ -0,0 +1 @@ += render "projects/snippets/form", url: project_snippets_path(@project, @snippet) diff --git a/app/views/projects/snippets/show.html.haml b/app/views/projects/snippets/show.html.haml new file mode 100644 index 00000000000..da93e4bdbb1 --- /dev/null +++ b/app/views/projects/snippets/show.html.haml @@ -0,0 +1,13 @@ +%h3.page_title + %i.icon-lock.cgreen + = @snippet.title + + %small.pull-right + = "##{@snippet.id}" + %span.light + by + = image_tag gravatar_icon(@snippet.author_email), class: "avatar avatar-inline s16" + = @snippet.author_name +%br +%div= render 'projects/snippets/blob' +%div#notes= render "notes/notes_with_form" diff --git a/app/views/shared/_clone_panel.html.haml b/app/views/shared/_clone_panel.html.haml index 62e0fa25898..5120902fa0e 100644 --- a/app/views/shared/_clone_panel.html.haml +++ b/app/views/shared/_clone_panel.html.haml @@ -4,10 +4,8 @@ = text_field_tag :project_clone, @project.url_to_repo, class: "one_click_select span7", readonly: true %span.add-on - if @project.public - .cblue - %i.icon-share - public + = public_icon + %span.cblue public - else - .cgreen - %i.icon-lock - private + = private_icon + %span.cgreen private diff --git a/app/views/snippets/_blob.html.haml b/app/views/snippets/_blob.html.haml index 017a33b34f3..6f62ea05205 100644 --- a/app/views/snippets/_blob.html.haml +++ b/app/views/snippets/_blob.html.haml @@ -3,7 +3,10 @@ %i.icon-file %strong= @snippet.file_name %span.options - = link_to "raw", raw_project_snippet_path(@project, @snippet), class: "btn btn-tiny", target: "_blank" + .btn-group.tree-btn-group.pull-right + - if @snippet.author == current_user + = link_to "Edit", edit_snippet_path(@snippet), class: "btn btn-tiny", title: 'Edit Snippet' + = link_to "Raw", raw_snippet_path(@snippet), class: "btn btn-tiny", target: "_blank" .file_content.code - unless @snippet.content.empty? %div{class: user_color_scheme_class} diff --git a/app/views/snippets/_form.html.haml b/app/views/snippets/_form.html.haml index 993a20058c6..95e9e0357bc 100644 --- a/app/views/snippets/_form.html.haml +++ b/app/views/snippets/_form.html.haml @@ -2,7 +2,7 @@ = @snippet.new_record? ? "New Snippet" : "Edit Snippet ##{@snippet.id}" %hr .snippet-form-holder - = form_for [@project, @snippet] do |f| + = form_for @snippet, as: :personal_snippet, url: url do |f| -if @snippet.errors.any? .alert.alert-error %ul @@ -13,6 +13,9 @@ = f.label :title .input= f.text_field :title, placeholder: "Example Snippet", class: 'input-xlarge', required: true .clearfix + = f.label "Private?" + .input= f.check_box :private, {class: ''} + .clearfix = f.label "Lifetime" .input= f.select :expires_at, lifetime_select_options, {}, {class: 'chosen span2'} .clearfix @@ -28,9 +31,9 @@ .form-actions = f.submit 'Save', class: "btn-save btn" - = link_to "Cancel", project_snippets_path(@project), class: " btn" + = link_to "Cancel", snippets_path(@project), class: " btn" - unless @snippet.new_record? - .pull-right= link_to 'Destroy', [@project, @snippet], confirm: 'Removed snippet cannot be restored! Are you sure?', method: :delete, class: "btn pull-right danger delete-snippet", id: "destroy_snippet_#{@snippet.id}" + .pull-right= link_to 'Destroy', snippet_path(@snippet), confirm: 'Removed snippet cannot be restored! Are you sure?', method: :delete, class: "btn pull-right danger delete-snippet", id: "destroy_snippet_#{@snippet.id}" :javascript diff --git a/app/views/snippets/_snippet.html.haml b/app/views/snippets/_snippet.html.haml index a576500c15d..194eb051ee3 100644 --- a/app/views/snippets/_snippet.html.haml +++ b/app/views/snippets/_snippet.html.haml @@ -1,13 +1,30 @@ -%tr - %td - = image_tag gravatar_icon(snippet.author_email), class: "avatar s24" - %a{href: project_snippet_path(snippet.project, snippet)} - %strong= truncate(snippet.title, length: 60) - %td - = snippet.file_name - %td +%li + .snippet-title + - if snippet.private? + = private_icon + - else + = public_icon + = link_to reliable_snippet_path(snippet) do + %h5.inline + = truncate(snippet.title, length: 60) %span.cgray - - if snippet.expires_at - = snippet.expires_at.to_date.to_s(:short) - - else - Never + = snippet.file_name + + %small.pull-right.cgray + - if snippet.project_id? + = link_to snippet.project.name_with_namespace, project_path(snippet.project) + %span + \| + Expires: + - if snippet.expires_at + = snippet.expires_at.to_date.to_s(:short) + - else + Never + + .snippet-info.prepend-left-20 + = "##{snippet.id}" + %span.light + by + = image_tag gravatar_icon(snippet.author_email), class: "avatar avatar-inline s16" + = snippet.author_name + diff --git a/app/views/snippets/_snippets.html.haml b/app/views/snippets/_snippets.html.haml new file mode 100644 index 00000000000..636bf37f6d9 --- /dev/null +++ b/app/views/snippets/_snippets.html.haml @@ -0,0 +1,7 @@ +%ul.bordered-list + = render partial: 'snippet', collection: @snippets + - if @snippets.empty? + %li + %h3.nothing_here_message Nothing here. + += paginate @snippets diff --git a/app/views/snippets/current_user_index.html.haml b/app/views/snippets/current_user_index.html.haml new file mode 100644 index 00000000000..912f4c77a4b --- /dev/null +++ b/app/views/snippets/current_user_index.html.haml @@ -0,0 +1,21 @@ +%h3.page_title + My Snippets + %small share code pastes with others out of git repository + = link_to new_snippet_path, class: "btn btn-small add_new pull-right", title: "New Snippet" do + Add new snippet + +%hr + +.row + .span3 + %ul.nav.nav-pills.nav-stacked + = nav_tab :scope, nil do + = link_to "All", user_snippets_path(@user) + = nav_tab :scope, 'private' do + = link_to "Private", user_snippets_path(@user, scope: 'private') + = nav_tab :scope, 'public' do + = link_to "Public", user_snippets_path(@user, scope: 'public') + + .span9 + = render 'snippets' + diff --git a/app/views/snippets/edit.html.haml b/app/views/snippets/edit.html.haml index f81c0b8bc64..1b88a85faf1 100644 --- a/app/views/snippets/edit.html.haml +++ b/app/views/snippets/edit.html.haml @@ -1 +1 @@ -= render "snippets/form" += render "snippets/form", url: snippet_path(@snippet) diff --git a/app/views/snippets/index.html.haml b/app/views/snippets/index.html.haml index bacf23d8f8d..97f7b39877e 100644 --- a/app/views/snippets/index.html.haml +++ b/app/views/snippets/index.html.haml @@ -1,19 +1,11 @@ %h3.page_title - Snippets + Public snippets %small share code pastes with others out of git repository + = link_to new_snippet_path, class: "btn btn-small add_new pull-right", title: "New Snippet" do + Add new snippet + +%hr +.row + .span12 + = render 'snippets' - - if can? current_user, :write_snippet, @project - = link_to new_project_snippet_path(@project), class: "btn btn-small add_new pull-right", title: "New Snippet" do - Add new snippet -%br -%table - %thead - %tr - %th Title - %th File Name - %th Expires At - = render @snippets - - if @snippets.empty? - %tr - %td{colspan: 3} - %h3.nothing_here_message Nothing here. diff --git a/app/views/snippets/new.html.haml b/app/views/snippets/new.html.haml index f81c0b8bc64..90e0a1f79da 100644 --- a/app/views/snippets/new.html.haml +++ b/app/views/snippets/new.html.haml @@ -1 +1 @@ -= render "snippets/form" += render "snippets/form", url: snippets_path(@snippet) diff --git a/app/views/snippets/show.html.haml b/app/views/snippets/show.html.haml index 12534edf8ba..f425c4bd51e 100644 --- a/app/views/snippets/show.html.haml +++ b/app/views/snippets/show.html.haml @@ -1,9 +1,16 @@ %h3.page_title + - if @snippet.private? + %i.icon-lock.cgreen + - else + %i.icon-globe.cblue + = @snippet.title - %small= @snippet.file_name - - if can?(current_user, :admin_snippet, @project) || @snippet.author == current_user - = link_to "Edit", edit_project_snippet_path(@project, @snippet), class: "btn btn-small pull-right", title: 'Edit Snippet' + %small.pull-right + = "##{@snippet.id}" + %span.light + by + = image_tag gravatar_icon(@snippet.author_email), class: "avatar avatar-inline s16" + = @snippet.author_name %br %div= render 'blob' -%div#notes= render "notes/notes_with_form" diff --git a/app/views/snippets/user_index.html.haml b/app/views/snippets/user_index.html.haml new file mode 100644 index 00000000000..79e3ddf96fd --- /dev/null +++ b/app/views/snippets/user_index.html.haml @@ -0,0 +1,13 @@ +%h3.page_title + = image_tag gravatar_icon(@user.email), class: "avatar s24" + = @user.name + %span + \/ + Snippets + %small share code pastes with others out of git repository + = link_to new_snippet_path, class: "btn btn-small add_new pull-right", title: "New Snippet" do + Add new snippet + +%hr + += render 'snippets' diff --git a/app/views/team_members/_team.html.haml b/app/views/team_members/_team.html.haml index 4b49b308edc..4ff170ac86e 100644 --- a/app/views/team_members/_team.html.haml +++ b/app/views/team_members/_team.html.haml @@ -1,3 +1,4 @@ +- can_admin_project = (can? current_user, :admin_project, @project) - team.each do |access, members| - role = Project.access_options.key(access).pluralize .ui-box{class: role.downcase} @@ -6,4 +7,4 @@ %span.light (#{members.size}) %ul.well-list - members.sort_by(&:user_name).each do |team_member| - = render 'team_members/team_member', member: team_member + = render 'team_members/team_member', member: team_member, current_user_can_admin_project: can_admin_project diff --git a/app/views/team_members/_team_member.html.haml b/app/views/team_members/_team_member.html.haml index 5fd8d2465d1..d829a79213c 100644 --- a/app/views/team_members/_team_member.html.haml +++ b/app/views/team_members/_team_member.html.haml @@ -1,5 +1,5 @@ - user = member.user -- allow_admin = can? current_user, :admin_project, @project +- allow_admin = current_user_can_admin_project %li{id: dom_id(user), class: "team_member_row user_#{user.id}"} .row .span4 diff --git a/app/views/teams/new.html.haml b/app/views/teams/new.html.haml index 99d308217e0..fe1f9cf10ec 100644 --- a/app/views/teams/new.html.haml +++ b/app/views/teams/new.html.haml @@ -1,5 +1,3 @@ -%h3.page_title New Team -%hr = form_for @team, url: teams_path do |f| - if @team.errors.any? .alert.alert-error @@ -15,16 +13,16 @@ .input = f.text_area :description, maxlength: 250, class: "xxlarge js-gfm-input", rows: 4 + + .clearfix + .input + %ul + %li All created teams are public (users can view who enter into team and which project are assigned for this team) + %li People within a team see only projects they have access to + %li You will be able to assign existing projects for team .form-actions = f.submit 'Create team', class: "btn btn-create" - .padded - %ul - %li All created teams are public (users can view who enter into team and which project are assigned for this team) - %li People within a team see only projects they have access to - %li You will be able to assign existing projects for team - %hr - - if current_user.can_create_group? .clearfix .input.light diff --git a/app/views/teams/show.atom.builder b/app/views/teams/show.atom.builder index 1468ac1d03c..fffb78d53e8 100644 --- a/app/views/teams/show.atom.builder +++ b/app/views/teams/show.atom.builder @@ -9,8 +9,8 @@ xml.feed "xmlns" => "http://www.w3.org/2005/Atom", "xmlns:media" => "http://sear @events.each do |event| if event.proper? xml.entry do - event_link = event.feed_url - event_title = event.feed_title + event_link = event_feed_url(event) + event_title = event_feed_title(event) xml.id "tag:#{request.host},#{event.created_at.strftime("%Y-%m-%d")}:#{event.id}" xml.link :href => event_link diff --git a/app/views/teams/show.html.haml b/app/views/teams/show.html.haml index 2ad7f743010..8582f85a2b5 100644 --- a/app/views/teams/show.html.haml +++ b/app/views/teams/show.html.haml @@ -12,7 +12,7 @@ .loading.hide .side.span4 - if @team.description.present? - .description.well.well-small.light + .description-block = @team.description = render "projects", projects: @projects .prepend-top-20 diff --git a/config/initializers/smtp_settings.rb.sample b/config/initializers/smtp_settings.rb.sample index 333c388f5ff..e62ad0f4b71 100644 --- a/config/initializers/smtp_settings.rb.sample +++ b/config/initializers/smtp_settings.rb.sample @@ -1,7 +1,7 @@ # To enable smtp email delivery for your GitLab instance do next: # 1. Change config/environments/production.rb to use smtp # config.action_mailer.delivery_method = :smtp -# 2. Rename this file to smpt_settings.rb +# 2. Rename this file to smtp_settings.rb # 3. Edit settings inside this file # 4. Restart GitLab instance # diff --git a/config/routes.rb b/config/routes.rb index 3609ac01891..6f72e2cb186 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -29,6 +29,7 @@ Gitlab::Application.routes.draw do # get 'help' => 'help#index' get 'help/api' => 'help#api' + get 'help/api/:category' => 'help#api', as: 'help_api_file' get 'help/markdown' => 'help#markdown' get 'help/permissions' => 'help#permissions' get 'help/public_access' => 'help#public_access' @@ -39,6 +40,16 @@ Gitlab::Application.routes.draw do get 'help/workflow' => 'help#workflow' # + # Global snippets + # + resources :snippets do + member do + get "raw" + end + end + get "/s/:username" => "snippets#user_index", as: :user_snippets, constraints: { username: /.*/ } + + # # Public namespace # namespace :public do @@ -179,9 +190,18 @@ Gitlab::Application.routes.draw do resources :commits, only: [:show], constraints: {id: /(?:[^.]|\.(?!atom$))+/, format: /atom/} resources :compare, only: [:index, :create] resources :blame, only: [:show], constraints: {id: /.+/} - resources :graph, only: [:show], constraints: {id: /(?:[^.]|\.(?!json$))+/, format: /json/} + resources :network, only: [:show], constraints: {id: /(?:[^.]|\.(?!json$))+/, format: /json/} + resources :graphs, only: [:show], constraints: {id: /(?:[^.]|\.(?!json$))+/, format: /json/} match "/compare/:from...:to" => "compare#show", as: "compare", via: [:get, :post], constraints: {from: /.+/, to: /.+/} + scope module: :projects do + resources :snippets do + member do + get "raw" + end + end + end + resources :wikis, only: [:show, :edit, :destroy, :create] do collection do get :pages @@ -231,11 +251,11 @@ Gitlab::Application.routes.draw do member do # tree viewer logs - get "logs_tree", constraints: { id: /[a-zA-Z.\/0-9_\-]+/ } + get "logs_tree", constraints: { id: /[a-zA-Z.\/0-9_\-#%+]+/ } get "logs_tree/:path" => "refs#logs_tree", as: :logs_file, constraints: { - id: /[a-zA-Z.0-9\/_\-]+/, + id: /[a-zA-Z.0-9\/_\-#%+]+/, path: /.*/ } end @@ -255,19 +275,12 @@ Gitlab::Application.routes.draw do end end - resources :snippets do - member do - get "raw" - end - end - resources :hooks, only: [:index, :create, :destroy] do member do get :test end end - resources :team, controller: 'team_members', only: [:index] resources :milestones, except: [:destroy] diff --git a/db/migrate/20130323174317_add_private_to_snippets.rb b/db/migrate/20130323174317_add_private_to_snippets.rb new file mode 100644 index 00000000000..92f3a5c7011 --- /dev/null +++ b/db/migrate/20130323174317_add_private_to_snippets.rb @@ -0,0 +1,5 @@ +class AddPrivateToSnippets < ActiveRecord::Migration + def change + add_column :snippets, :private, :boolean, null: false, default: true + end +end diff --git a/db/migrate/20130324151736_add_type_to_snippets.rb b/db/migrate/20130324151736_add_type_to_snippets.rb new file mode 100644 index 00000000000..276aab2ca15 --- /dev/null +++ b/db/migrate/20130324151736_add_type_to_snippets.rb @@ -0,0 +1,5 @@ +class AddTypeToSnippets < ActiveRecord::Migration + def change + add_column :snippets, :type, :string + end +end diff --git a/db/migrate/20130324172327_change_project_id_to_null_in_snipepts.rb b/db/migrate/20130324172327_change_project_id_to_null_in_snipepts.rb new file mode 100644 index 00000000000..4c992bac4d1 --- /dev/null +++ b/db/migrate/20130324172327_change_project_id_to_null_in_snipepts.rb @@ -0,0 +1,9 @@ +class ChangeProjectIdToNullInSnipepts < ActiveRecord::Migration + def up + change_column :snippets, :project_id, :integer, :null => true + end + + def down + change_column :snippets, :project_id, :integer, :null => false + end +end diff --git a/db/migrate/20130324203535_add_type_value_for_snippets.rb b/db/migrate/20130324203535_add_type_value_for_snippets.rb new file mode 100644 index 00000000000..8c05dd2cc71 --- /dev/null +++ b/db/migrate/20130324203535_add_type_value_for_snippets.rb @@ -0,0 +1,8 @@ +class AddTypeValueForSnippets < ActiveRecord::Migration + def up + Snippet.where("project_id IS NOT NULL").update_all(type: 'ProjectSnippet') + end + + def down + end +end diff --git a/db/schema.rb b/db/schema.rb index 6a16caf59d8..21e553dd612 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -203,12 +203,14 @@ ActiveRecord::Schema.define(:version => 20130522141856) do create_table "snippets", :force => true do |t| t.string "title" t.text "content" - t.integer "author_id", :null => false - t.integer "project_id", :null => false - t.datetime "created_at", :null => false - t.datetime "updated_at", :null => false + t.integer "author_id", :null => false + t.integer "project_id" + t.datetime "created_at", :null => false + t.datetime "updated_at", :null => false t.string "file_name" t.datetime "expires_at" + t.boolean "private", :default => true, :null => false + t.string "type" end add_index "snippets", ["created_at"], :name => "index_snippets_on_created_at" diff --git a/doc/api/README.md b/doc/api/README.md index c3cfc0e9f42..9120fe3aea4 100644 --- a/doc/api/README.md +++ b/doc/api/README.md @@ -72,11 +72,12 @@ When listing resources you can pass the following parameters: + [Users](doc/api/users.md) + [Session](doc/api/session.md) + [Projects](doc/api/projects.md) -+ [Groups](doc/api/groups.md) -+ [Snippets](doc/api/snippets.md) ++ [Project Snippets](doc/api/project_snippets.md) + [Repositories](doc/api/repositories.md) + [Issues](doc/api/issues.md) + [Milestones](doc/api/milestones.md) + [Notes](doc/api/notes.md) ++ [Deploy Keys](doc/api/deploy_keys.md) + [System Hooks](doc/api/system_hooks.md) ++ [Groups](doc/api/groups.md) + [User Teams](doc/api/user_teams.md) diff --git a/doc/api/deploy_keys.md b/doc/api/deploy_keys.md new file mode 100644 index 00000000000..5a394094fe6 --- /dev/null +++ b/doc/api/deploy_keys.md @@ -0,0 +1,87 @@ +## Deploy Keys + +### List deploy keys + +Get a list of a project's deploy keys. + +``` +GET /projects/:id/keys +``` + +Parameters: + ++ `id` (required) - The ID of the project + +```json +[ + { + "id": 1, + "title" : "Public key" + "key": "ssh-rsa AAAAB3NzaC1yc2EAAAABJQAAAIEAiPWx6WM4lhHNedGfBpPJNPpZ7yKu+dnn1SJejgt4 + 596k6YjzGGphH2TUxwKzxcKDKKezwkpfnxPkSMkuEspGRt/aZZ9wa++Oi7Qkr8prgHc4 + soW6NUlfDzpvZK2H5E7eQaSeP3SAwGmQKUFHCddNaP0L+hM7zhFNzjFvpaMgJw0=", + }, + { + "id": 3, + "title" : "Another Public key" + "key": "ssh-rsa AAAAB3NzaC1yc2EAAAABJQAAAIEAiPWx6WM4lhHNedGfBpPJNPpZ7yKu+dnn1SJejgt4 + 596k6YjzGGphH2TUxwKzxcKDKKezwkpfnxPkSMkuEspGRt/aZZ9wa++Oi7Qkr8prgHc4 + soW6NUlfDzpvZK2H5E7eQaSeP3SAwGmQKUFHCddNaP0L+hM7zhFNzjFvpaMgJw0=" + } +] +``` + + +### Single deploy key + +Get a single key. + +``` +GET /projects/:id/keys/:key_id +``` + +Parameters: + ++ `id` (required) - The ID of the project ++ `key_id` (required) - The ID of the deploy key + +```json +{ + "id": 1, + "title" : "Public key" + "key": "ssh-rsa AAAAB3NzaC1yc2EAAAABJQAAAIEAiPWx6WM4lhHNedGfBpPJNPpZ7yKu+dnn1SJejgt4 + 596k6YjzGGphH2TUxwKzxcKDKKezwkpfnxPkSMkuEspGRt/aZZ9wa++Oi7Qkr8prgHc4 + soW6NUlfDzpvZK2H5E7eQaSeP3SAwGmQKUFHCddNaP0L+hM7zhFNzjFvpaMgJw0=" +} +``` + + +### Add deploy key + +Creates a new deploy key for a project. +If deploy key already exists in another project - it will be joined to project but only if original one was is accessible by same user + +``` +POST /projects/:id/keys +``` + +Parameters: + ++ `id` (required) - The ID of the project ++ `title` (required) - New deploy key's title ++ `key` (required) - New deploy key + + +### Delete deploy key + +Delete a deploy key from a project + +``` +DELETE /projects/:id/keys/:key_id +``` + +Parameters: + ++ `id` (required) - The ID of the project ++ `key_id` (required) - The ID of the deploy key + diff --git a/doc/api/issues.md b/doc/api/issues.md index a8ae7401b39..87547593586 100644 --- a/doc/api/issues.md +++ b/doc/api/issues.md @@ -25,7 +25,7 @@ GET /issues "blocked": false, "created_at": "2012-05-23T08:00:58Z" }, - "closed": true, + "state": 'closed', "updated_at": "2012-07-02T17:53:12Z", "created_at": "2012-07-02T17:53:12Z" }, @@ -42,7 +42,7 @@ GET /issues "title": "v1.0", "description": "", "due_date": "2012-07-20", - "closed": false, + "state": 'reopenend', "updated_at": "2012-07-04T13:42:48Z", "created_at": "2012-07-04T13:42:48Z" }, @@ -62,7 +62,7 @@ GET /issues "blocked": false, "created_at": "2012-05-23T08:00:58Z" }, - "closed": false, + "state": 'opened', "updated_at": "2012-07-12T13:43:19Z", "created_at": "2012-06-28T12:58:06Z" } @@ -111,7 +111,7 @@ Parameters: "title": "v1.0", "description": "", "due_date": "2012-07-20", - "closed": false, + "state": 'closed', "updated_at": "2012-07-04T13:42:48Z", "created_at": "2012-07-04T13:42:48Z" }, @@ -131,7 +131,7 @@ Parameters: "blocked": false, "created_at": "2012-05-23T08:00:58Z" }, - "closed": false, + "state": 'opened', "updated_at": "2012-07-12T13:43:19Z", "created_at": "2012-06-28T12:58:06Z" } @@ -173,7 +173,7 @@ Parameters: + `assignee_id` (optional) - The ID of a user to assign issue + `milestone_id` (optional) - The ID of a milestone to assign issue + `labels` (optional) - Comma-separated label names for an issue -+ `closed` (optional) - The state of an issue (0 = false, 1 = true) ++ `state_event` (optional) - The state event of an issue ('close' to close issue and 'reopen' to reopen it) ## Delete existing issue (**Deprecated**) diff --git a/doc/api/milestones.md b/doc/api/milestones.md index 92a29cee954..aa8f1bf5e02 100644 --- a/doc/api/milestones.md +++ b/doc/api/milestones.md @@ -56,5 +56,5 @@ Parameters: + `title` (optional) - The title of a milestone + `description` (optional) - The description of a milestone + `due_date` (optional) - The due date of the milestone -+ `closed` (optional) - The status of the milestone ++ `state_event` (optional) - The state event of the milestone (close|activate) diff --git a/doc/api/snippets.md b/doc/api/project_snippets.md index 04ea367d518..04ea367d518 100644 --- a/doc/api/snippets.md +++ b/doc/api/project_snippets.md diff --git a/doc/api/projects.md b/doc/api/projects.md index d3d15ab4e02..323c0be63a4 100644 --- a/doc/api/projects.md +++ b/doc/api/projects.md @@ -30,7 +30,8 @@ GET /projects "merge_requests_enabled": false, "wall_enabled": true, "wiki_enabled": true, - "created_at": "2012-05-23T08:05:02Z" + "created_at": "2012-05-23T08:05:02Z", + "last_activity_at": "2012-05-23T08:05:02Z" }, { "id": 5, @@ -52,7 +53,8 @@ GET /projects "merge_requests_enabled": true, "wall_enabled": true, "wiki_enabled": true, - "created_at": "2012-05-30T12:49:20Z" + "created_at": "2012-05-30T12:49:20Z", + "last_activity_at": "2012-05-23T08:05:02Z" } ] ``` @@ -75,6 +77,7 @@ Parameters: { "id": 5, "name": "gitlab", + "name_with_namespace": "GitLab / gitlabhq", "description": null, "default_branch": "api", "owner": { @@ -92,10 +95,79 @@ Parameters: "merge_requests_enabled": true, "wall_enabled": true, "wiki_enabled": true, - "created_at": "2012-05-30T12:49:20Z" + "created_at": "2012-05-30T12:49:20Z", + "last_activity_at": "2012-05-23T08:05:02Z" } ``` +### Get project events + +Get a project events for specific project. +Sorted from newest to latest + +``` +GET /projects/:id/events +``` + +Parameters: + ++ `id` (required) - The ID or NAME of a project + +```json + +[{ + "title": null, + "project_id": 15, + "action_name": "closed", + "target_id": 830, + "target_type": "Issue", + "author_id": 1, + "data": null, + "target_title": "Public project search field" +}, { + "title": null, + "project_id": 15, + "action_name": "opened", + "target_id": null, + "target_type": null, + "author_id": 1, + "data": { + "before": "50d4420237a9de7be1304607147aec22e4a14af7", + "after": "c5feabde2d8cd023215af4d2ceeb7a64839fc428", + "ref": "refs/heads/master", + "user_id": 1, + "user_name": "Dmitriy Zaporozhets", + "repository": { + "name": "gitlabhq", + "url": "git@dev.gitlab.org:gitlab/gitlabhq.git", + "description": "GitLab: self hosted Git management software. \r\nDistributed under the MIT License.", + "homepage": "https://dev.gitlab.org/gitlab/gitlabhq" + }, + "commits": [{ + "id": "c5feabde2d8cd023215af4d2ceeb7a64839fc428", + "message": "Add simple search to projects in public area", + "timestamp": "2013-05-13T18:18:08+00:00", + "url": "https://dev.gitlab.org/gitlab/gitlabhq/commit/c5feabde2d8cd023215af4d2ceeb7a64839fc428", + "author": { + "name": "Dmitriy Zaporozhets", + "email": "dmitriy.zaporozhets@gmail.com" + } + }], + "total_commits_count": 1 + }, + "target_title": null +}, { + "title": null, + "project_id": 15, + "action_name": "closed", + "target_id": 840, + "target_type": "Issue", + "author_id": 1, + "data": null, + "target_title": "Finish & merge Code search PR" +}] +``` + ### Create project @@ -381,126 +453,3 @@ Parameters: + `id` (required) - The ID of the project. + `branch` (required) - The name of the branch. - -### List tags - -Lists all tags of a project. - -``` -GET /projects/:id/repository/tags -``` - -Parameters: - -+ `id` (required) - The ID of the project - - -### List commits - -Lists all commits with pagination. If the optional `ref_name` name is not given the commits of -the default branch (usually master) are returned. - -``` -GET /projects/:id/repository/commits -``` - -Parameters: - -+ `id` (required) - The Id of the project -+ `ref_name` (optional) - The name of a repository branch or tag -+ `page` (optional) - The page of commits to return (`0` default) -+ `per_page` (optional) - The number of commits per page (`20` default) - -Returns values: - -+ `200 Ok` on success and a list with commits -+ `404 Not Found` if project with id or the branch with `ref_name` not found - - - -## Deploy Keys - -### List deploy keys - -Get a list of a project's deploy keys. - -``` -GET /projects/:id/keys -``` - -Parameters: - -+ `id` (required) - The ID of the project - -```json -[ - { - "id": 1, - "title" : "Public key" - "key": "ssh-rsa AAAAB3NzaC1yc2EAAAABJQAAAIEAiPWx6WM4lhHNedGfBpPJNPpZ7yKu+dnn1SJejgt4 - 596k6YjzGGphH2TUxwKzxcKDKKezwkpfnxPkSMkuEspGRt/aZZ9wa++Oi7Qkr8prgHc4 - soW6NUlfDzpvZK2H5E7eQaSeP3SAwGmQKUFHCddNaP0L+hM7zhFNzjFvpaMgJw0=", - }, - { - "id": 3, - "title" : "Another Public key" - "key": "ssh-rsa AAAAB3NzaC1yc2EAAAABJQAAAIEAiPWx6WM4lhHNedGfBpPJNPpZ7yKu+dnn1SJejgt4 - 596k6YjzGGphH2TUxwKzxcKDKKezwkpfnxPkSMkuEspGRt/aZZ9wa++Oi7Qkr8prgHc4 - soW6NUlfDzpvZK2H5E7eQaSeP3SAwGmQKUFHCddNaP0L+hM7zhFNzjFvpaMgJw0=" - } -] -``` - - -### Single deploy key - -Get a single key. - -``` -GET /projects/:id/keys/:key_id -``` - -Parameters: - -+ `id` (required) - The ID of the project -+ `key_id` (required) - The ID of the deploy key - -```json -{ - "id": 1, - "title" : "Public key" - "key": "ssh-rsa AAAAB3NzaC1yc2EAAAABJQAAAIEAiPWx6WM4lhHNedGfBpPJNPpZ7yKu+dnn1SJejgt4 - 596k6YjzGGphH2TUxwKzxcKDKKezwkpfnxPkSMkuEspGRt/aZZ9wa++Oi7Qkr8prgHc4 - soW6NUlfDzpvZK2H5E7eQaSeP3SAwGmQKUFHCddNaP0L+hM7zhFNzjFvpaMgJw0=" -} -``` - - -### Add deploy key - -Creates a new deploy key for a project. - -``` -POST /projects/:id/keys -``` - -Parameters: - -+ `id` (required) - The ID of the project -+ `title` (required) - New deploy key's title -+ `key` (required) - New deploy key - - -### Delete deploy key - -Delete a deploy key from a project - -``` -DELETE /projects/:id/keys/:key_id -``` - -Parameters: - -+ `id` (required) - The ID of the project -+ `key_id` (required) - The ID of the deploy key - diff --git a/doc/api/repositories.md b/doc/api/repositories.md index 90fda387947..7a9f766ba1d 100644 --- a/doc/api/repositories.md +++ b/doc/api/repositories.md @@ -239,6 +239,56 @@ Parameters: ] ``` +## List repository tree + +Get a list of repository files and directories in a project. + +``` +GET /projects/:id/repository/tree +``` + +Parameters: + ++ `id` (required) - The ID of a project ++ `path` (optional) - The path inside repository. Used to get contend of subdirectories ++ `ref_name` (optional) - The name of a repository branch or tag or if not given the default branch + +```json + +[{ + "name": "assets", + "type": "tree", + "mode": "040000", + "id": "6229c43a7e16fcc7e95f923f8ddadb8281d9c6c6" +}, { + "name": "contexts", + "type": "tree", + "mode": "040000", + "id": "faf1cdf33feadc7973118ca42d35f1e62977e91f" +}, { + "name": "controllers", + "type": "tree", + "mode": "040000", + "id": "95633e8d258bf3dfba3a5268fb8440d263218d74" +}, { + "name": "Rakefile", + "type": "blob", + "mode": "100644", + "id": "35b2f05cbb4566b71b34554cf184a9d0bd9d46d6" +}, { + "name": "VERSION", + "type": "blob", + "mode": "100644", + "id": "803e4a4f3727286c3093c63870c2b6524d30ec4f" +}, { + "name": "config.ru", + "type": "blob", + "mode": "100644", + "id": "dfd2d862237323aa599be31b473d70a8a817943b" +}] + +``` + ## Raw blob content diff --git a/doc/install/installation.md b/doc/install/installation.md index 2b3e40034ad..3b459ad77b5 100644 --- a/doc/install/installation.md +++ b/doc/install/installation.md @@ -188,6 +188,10 @@ do so with caution! # Copy the example Puma config sudo -u git -H cp config/puma.rb.example config/puma.rb + # Enable cluster mode if you expect to have a high load instance + # Ex. change amount of workers to 3 for 2GB RAM server + sudo -u git -H vim config/puma.rb + # Configure Git global settings for git user, useful when editing via web # Edit user.email according to what is set in gitlab.yml sudo -u git -H git config --global user.name "GitLab" diff --git a/doc/make_release.md b/doc/make_release.md new file mode 100644 index 00000000000..24f8397f10b --- /dev/null +++ b/doc/make_release.md @@ -0,0 +1,56 @@ +# Things to do when creating new release +NOTE: This is a developer guide. If you are trying to install GitLab see the latest stable [installation guide](install/installation.md) and if you are trying to upgrade, see the [upgrade guides](update). +## Install guide up to date? + +* References correct GitLab branch `x-x-stable` and correct GitLab shell tag? + +## Make upgrade guide + +### From x.x to x.x + +#### 0. Any major changes? Database updates? Web server change? File strucuture changes? + +#### 1. Make backup + +#### 2. Stop server + +#### 3. Do users need to update dependencies like `git`? + +#### 4. Get latest code + +#### 5. Does GitLab shell need to be updated? + +#### 6. Install libs, migrations, etc. + +#### 7. Any config files updated since last release? + +Check if any of these changed since last release (~22nd of last month depending on when last release branch was created): + +* https://github.com/gitlabhq/gitlabhq/commits/master/lib/support/nginx/gitlab +* https://github.com/gitlabhq/gitlab-shell/commits/master/config.yml.example +* https://github.com/gitlabhq/gitlabhq/commits/master/config/gitlab.yml.example +* https://github.com/gitlabhq/gitlabhq/commits/master/config/puma.rb.example +* https://github.com/gitlabhq/gitlabhq/commits/master/config/database.yml.mysql +* https://github.com/gitlabhq/gitlabhq/commits/master/config/database.yml.postgresql + +#### 8. Need to update init script? + +Check if changed since last release (~22nd of last month depending on when last release branch was created): https://github.com/gitlabhq/gitlabhq/commits/master/lib/support/init.d/gitlab + +#### 9. Start application + +#### 10. Check application status + +## Make sure code status is good + +* [](http://ci.gitlab.org/projects/1?ref=master) on ci.gitlab.org (master branch) + +* [](https://travis-ci.org/gitlabhq/gitlabhq) on travis-ci.org (master branch) + +* [](https://codeclimate.com/github/gitlabhq/gitlabhq) + +* [](https://gemnasium.com/gitlabhq/gitlabhq) this button can be yellow (small updates are available) but must not be red (a security fix or an important update is available) + +* [](https://coveralls.io/r/gitlabhq/gitlabhq) + +## Make release branch diff --git a/doc/update/5.1-to-5.2.md b/doc/update/5.1-to-5.2.md index 8f2116837b8..3f874555f49 100644 --- a/doc/update/5.1-to-5.2.md +++ b/doc/update/5.1-to-5.2.md @@ -1,5 +1,15 @@ # From 5.1 to 5.2 +### 0. Backup + +It's useful to make a backup just in case things go south: +(With MySQL, this may require granting "LOCK TABLES" privileges to the GitLab user on the database version) + +```bash +cd /home/git/gitlab +sudo -u git -H RAILS_ENV=production bundle exec rake gitlab:backup:create +``` + ### 1. Stop server sudo service gitlab stop @@ -20,14 +30,59 @@ sudo -u git -H git fetch sudo -u git -H git checkout v1.4.0 ``` -### 4. Install libs, migrations etc +### 4. Install libs, migrations, etc. ```bash cd /home/git/gitlab + +# MySQL sudo -u git -H bundle install --without development test postgres --deployment + +#PostgreSQL +sudo -u git -H bundle install --without development test mysql --deployment + sudo -u git -H bundle exec rake db:migrate RAILS_ENV=production ``` -### 5. Start application +### 5. Update config files + +* Make `/home/git/gitlab/config/gitlab.yml` same as https://github.com/gitlabhq/gitlabhq/blob/5-2-stable/config/gitlab.yml.example but with your settings. +* Make `/home/git/gitlab/config/puma.rb` same as https://github.com/gitlabhq/gitlabhq/blob/5-2-stable/config/puma.rb.example but with your settings. + +### 6. Update Init script + +```bash +sudo rm /etc/init.d/gitlab +sudo curl --output /etc/init.d/gitlab https://raw.github.com/gitlabhq/gitlabhq/5-2-stable/lib/support/init.d/gitlab +sudo chmod +x /etc/init.d/gitlab +``` + +### 6. Start application sudo service gitlab start + sudo service nginx restart + +### 7. Check application status + +Check if GitLab and its environment are configured correctly: + + sudo -u git -H bundle exec rake gitlab:env:info RAILS_ENV=production + +To make sure you didn't miss anything run a more thorough check with: + + sudo -u git -H bundle exec rake gitlab:check RAILS_ENV=production + +If all items are green, then congratulations upgrade complete! + +## Things went south? Revert to previous version (5.1) + +### 1. Revert the code to the previous version +Follow the [`upgrade guide from 5.0 to 5.1`](5.0-to-5.1.md), except for the database migration +(The backup is already migrated to the previous version) + +### 2. Restore from the backup: + +```bash +cd /home/git/gitlab +sudo -u git -H RAILS_ENV=production bundle exec rake gitlab:backup:restore +``` diff --git a/features/admin/groups.feature b/features/admin/groups.feature index 28f35e3a831..054dccfd64c 100644 --- a/features/admin/groups.feature +++ b/features/admin/groups.feature @@ -11,6 +11,7 @@ Feature: Admin Groups Then I should be redirected to group page And I should see newly created group + @javascript Scenario: Add user into projects in group When I visit admin group page When I select user "John" from user list as "Reporter" diff --git a/features/dashboard/active_tab.feature b/features/dashboard/active_tab.feature index 6715ea269c7..3e1cf5aa0ca 100644 --- a/features/dashboard/active_tab.feature +++ b/features/dashboard/active_tab.feature @@ -17,11 +17,6 @@ Feature: Dashboard active tab Then the active main tab should be Merge Requests And no other main tabs should be active - Scenario: On Dashboard Search - Given I visit dashboard search page - Then the active main tab should be Search - And no other main tabs should be active - Scenario: On Dashboard Help Given I visit dashboard help page Then the active main tab should be Help diff --git a/features/group/group.feature b/features/group/group.feature index a48affe8e02..64424f47236 100644 --- a/features/group/group.feature +++ b/features/group/group.feature @@ -19,6 +19,7 @@ Feature: Groups When I visit group merge requests page Then I should see merge requests from this group assigned to me + @javascript Scenario: I should add user to projects in Group Given I have new user "John" When I visit group people page diff --git a/features/project/graph.feature b/features/project/graph.feature new file mode 100644 index 00000000000..cda95f5dda6 --- /dev/null +++ b/features/project/graph.feature @@ -0,0 +1,9 @@ +Feature: Project Graph + Background: + Given I sign in as a user + And I own project "Shop" + And I visit project "Shop" graph page + + @javascript + Scenario: I should see project graphs + Then page should have graphs diff --git a/features/project/project.feature b/features/project/project.feature index 0c8c97c816a..59eda4a781d 100644 --- a/features/project/project.feature +++ b/features/project/project.feature @@ -5,6 +5,7 @@ Feature: Project Feature And project "Shop" has push event And I visit project "Shop" page + @javascript Scenario: I should see project activity When I visit project "Shop" page Then I should see project "Shop" activity feed diff --git a/features/project/snippets.feature b/features/project/snippets.feature new file mode 100644 index 00000000000..a26c8dc8474 --- /dev/null +++ b/features/project/snippets.feature @@ -0,0 +1,35 @@ +Feature: Project Snippets + Background: + Given I sign in as a user + And I own project "Shop" + And project "Shop" have "Snippet one" snippet + And project "Shop" have no "Snippet two" snippet + And I visit project "Shop" snippets page + + Scenario: I should see snippets + Given I visit project "Shop" snippets page + Then I should see "Snippet one" in snippets + And I should not see "Snippet two" in snippets + + Scenario: I create new project snippet + Given I click link "New Snippet" + And I submit new snippet "Snippet three" + Then I should see snippet "Snippet three" + + @javascript + Scenario: I comment on a snippet "Snippet one" + Given I visit snippet page "Snippet one" + And I leave a comment like "Good snippet!" + Then I should see comment "Good snippet!" + + Scenario: I update "Snippet one" + Given I visit snippet page "Snippet one" + And I click link "Edit" + And I submit new title "Snippet new title" + Then I should see "Snippet new title" + + Scenario: I destroy "Snippet one" + Given I visit snippet page "Snippet one" + And I click link "Edit" + And I click link "Destroy" + Then I should not see "Snippet one" in snippets diff --git a/features/snippets/discover_snippets.feature b/features/snippets/discover_snippets.feature new file mode 100644 index 00000000000..d6fd2cd7808 --- /dev/null +++ b/features/snippets/discover_snippets.feature @@ -0,0 +1,10 @@ +Feature: Discover Snippets + Background: + Given I sign in as a user + And I have public "Personal snippet one" snippet + And I have private "Personal snippet private" snippet + + Scenario: I should see snippets + Given I visit snippets page + Then I should see "Personal snippet one" in snippets + And I should not see "Personal snippet private" in snippets diff --git a/features/snippets/snippets.feature b/features/snippets/snippets.feature new file mode 100644 index 00000000000..1119defa17d --- /dev/null +++ b/features/snippets/snippets.feature @@ -0,0 +1,28 @@ +Feature: Snippets Feature + Background: + Given I sign in as a user + And I have public "Personal snippet one" snippet + And I have private "Personal snippet private" snippet + + Scenario: I create new snippet + Given I visit new snippet page + And I submit new snippet "Personal snippet three" + Then I should see snippet "Personal snippet three" + + Scenario: I update "Personal snippet one" + Given I visit snippet page "Personal snippet one" + And I click link "Edit" + And I submit new title "Personal snippet new title" + Then I should see "Personal snippet new title" + + Scenario: Set "Personal snippet one" public + Given I visit snippet page "Personal snippet one" + And I click link "Edit" + And I uncheck "Private" checkbox + Then I should see "Personal snippet one" public + + Scenario: I destroy "Personal snippet one" + Given I visit snippet page "Personal snippet one" + And I click link "Edit" + And I click link "Destroy" + Then I should not see "Personal snippet one" in snippets diff --git a/features/snippets/user_snippets.feature b/features/snippets/user_snippets.feature new file mode 100644 index 00000000000..4c8a91501c4 --- /dev/null +++ b/features/snippets/user_snippets.feature @@ -0,0 +1,22 @@ +Feature: User Snippets + Background: + Given I sign in as a user + And I have public "Personal snippet one" snippet + And I have private "Personal snippet private" snippet + + Scenario: I should see all my snippets + Given I visit my snippets page + Then I should see "Personal snippet one" in snippets + And I should see "Personal snippet private" in snippets + + Scenario: I can see only my private snippets + Given I visit my snippets page + And I click "Private" filter + Then I should not see "Personal snippet one" in snippets + And I should see "Personal snippet private" in snippets + + Scenario: I can see only my public snippets + Given I visit my snippets page + And I click "Public" filter + Then I should see "Personal snippet one" in snippets + And I should not see "Personal snippet private" in snippets diff --git a/features/steps/admin/admin_groups.rb b/features/steps/admin/admin_groups.rb index a2b49070f9f..d780d9c96d9 100644 --- a/features/steps/admin/admin_groups.rb +++ b/features/steps/admin/admin_groups.rb @@ -2,6 +2,7 @@ class AdminGroups < Spinach::FeatureSteps include SharedAuthentication include SharedPaths include SharedActiveTab + include Select2Helper When 'I visit admin group page' do visit admin_group_path(current_group) @@ -40,8 +41,8 @@ class AdminGroups < Spinach::FeatureSteps When 'I select user "John" from user list as "Reporter"' do user = User.find_by_name("John") + select2(user.id, from: "#user_ids", multiple: true) within "#new_team_member" do - select user.name, from: "user_ids" select "Reporter", from: "project_access" end click_button "Add user to projects in group" @@ -49,8 +50,6 @@ class AdminGroups < Spinach::FeatureSteps Then 'I should see "John" in team list in every project as "Reporter"' do user = User.find_by_name("John") - projects_with_access = find(".user_#{user.id} .projects_access") - projects_with_access.should have_link("Reporter") end protected diff --git a/features/steps/admin/admin_teams.rb b/features/steps/admin/admin_teams.rb index 65c7e485f48..066fc3fa603 100644 --- a/features/steps/admin/admin_teams.rb +++ b/features/steps/admin/admin_teams.rb @@ -85,7 +85,7 @@ class AdminTeams < Spinach::FeatureSteps end Then 'I should see empty projects table' do - page.has_no_css?("#projects_list").must_equal true + page.should have_content "Projects (0)" end When 'I select project "Shop" with max access "Reporter"' do diff --git a/features/steps/dashboard/dashboard_active_tab.rb b/features/steps/dashboard/dashboard_active_tab.rb index 41ecc48c0d3..8f5f0eed816 100644 --- a/features/steps/dashboard/dashboard_active_tab.rb +++ b/features/steps/dashboard/dashboard_active_tab.rb @@ -15,10 +15,6 @@ class DashboardActiveTab < Spinach::FeatureSteps ensure_active_main_tab('Merge Requests') end - Then 'the active main tab should be Search' do - ensure_active_main_tab('Search') - end - Then 'the active main tab should be Help' do ensure_active_main_tab('Help') end diff --git a/features/steps/group/group.rb b/features/steps/group/group.rb index 8b5a4ed44df..102bc440d82 100644 --- a/features/steps/group/group.rb +++ b/features/steps/group/group.rb @@ -1,6 +1,7 @@ class Groups < Spinach::FeatureSteps include SharedAuthentication include SharedPaths + include Select2Helper Then 'I should see projects list' do current_user.authorized_projects.each do |project| @@ -39,7 +40,7 @@ class Groups < Spinach::FeatureSteps And 'I select user "John" from list with role "Reporter"' do user = User.find_by_name("John") within "#new_team_member" do - select user.name, from: "user_ids" + select2(user.id, from: "#user_ids", multiple: true) select "Reporter", from: "project_access" end click_button "Add" diff --git a/features/steps/profile/profile.rb b/features/steps/profile/profile.rb index 8981705df48..c1fe00c8e65 100644 --- a/features/steps/profile/profile.rb +++ b/features/steps/profile/profile.rb @@ -74,7 +74,7 @@ class Profile < Spinach::FeatureSteps When "I change my code preview theme" do within '.code-preview-theme' do - choose "Solarized Dark" + choose "Solarized dark" end end diff --git a/features/steps/project/deploy_keys.rb b/features/steps/project/deploy_keys.rb index fd9dce7fe30..7f7492bfd6d 100644 --- a/features/steps/project/deploy_keys.rb +++ b/features/steps/project/deploy_keys.rb @@ -35,6 +35,7 @@ class Spinach::Features::ProjectDeployKeys < Spinach::FeatureSteps step 'other project has deploy key' do @second_project = create :project, namespace: current_user.namespace + @second_project.team << [current_user, :master] create(:deploy_keys_project, project: @second_project) end diff --git a/features/steps/project/project_graph.rb b/features/steps/project/project_graph.rb new file mode 100644 index 00000000000..50942b3cbb3 --- /dev/null +++ b/features/steps/project/project_graph.rb @@ -0,0 +1,13 @@ +class ProjectGraph < Spinach::FeatureSteps + include SharedAuthentication + include SharedProject + + Then 'page should have graphs' do + page.should have_selector ".stat-graph" + end + + When 'I visit project "Shop" graph page' do + project = Project.find_by_name("Shop") + visit project_graph_path(project, "master") + end +end diff --git a/features/steps/project/project_network_graph.rb b/features/steps/project/project_network_graph.rb index 48a73f09fac..f001c0beb9a 100644 --- a/features/steps/project/project_network_graph.rb +++ b/features/steps/project/project_network_graph.rb @@ -12,7 +12,7 @@ class ProjectNetworkGraph < Spinach::FeatureSteps Network::Graph.stub(max_count: 10) project = Project.find_by_name("Shop") - visit project_graph_path(project, "master") + visit project_network_path(project, "master") end And 'page should select "master" in select box' do diff --git a/features/steps/project/project_snippets.rb b/features/steps/project/project_snippets.rb new file mode 100644 index 00000000000..2634ea192bf --- /dev/null +++ b/features/steps/project/project_snippets.rb @@ -0,0 +1,100 @@ +class ProjectSnippets < Spinach::FeatureSteps + include SharedAuthentication + include SharedProject + include SharedNote + include SharedPaths + + And 'project "Shop" have "Snippet one" snippet' do + create(:project_snippet, + title: "Snippet one", + content: "Test content", + file_name: "snippet.rb", + project: project, + author: project.users.first) + end + + And 'project "Shop" have no "Snippet two" snippet' do + create(:snippet, + title: "Snippet two", + content: "Test content", + file_name: "snippet.rb", + author: project.users.first) + end + + Given 'I click link "New Snippet"' do + click_link "Add new snippet" + end + + Given 'I click link "Snippet one"' do + click_link "Snippet one" + end + + Then 'I should see "Snippet one" in snippets' do + page.should have_content "Snippet one" + end + + And 'I should not see "Snippet two" in snippets' do + page.should_not have_content "Snippet two" + end + + And 'I should not see "Snippet one" in snippets' do + page.should_not have_content "Snippet one" + end + + And 'I click link "Edit"' do + within ".file_title" do + click_link "Edit" + end + end + + And 'I click link "Destroy"' do + click_link "Destroy" + end + + And 'I submit new snippet "Snippet three"' do + fill_in "project_snippet_title", :with => "Snippet three" + select "forever", :from => "project_snippet_expires_at" + fill_in "project_snippet_file_name", :with => "my_snippet.rb" + within('.file-editor') do + find(:xpath, "//input[@id='project_snippet_content']").set 'Content of snippet three' + end + click_button "Save" + end + + Then 'I should see snippet "Snippet three"' do + page.should have_content "Snippet three" + page.should have_content "Content of snippet three" + end + + And 'I submit new title "Snippet new title"' do + fill_in "project_snippet_title", :with => "Snippet new title" + click_button "Save" + end + + Then 'I should see "Snippet new title"' do + page.should have_content "Snippet new title" + end + + And 'I leave a comment like "Good snippet!"' do + within('.js-main-target-form') do + fill_in "note_note", with: "Good snippet!" + click_button "Add Comment" + end + end + + Then 'I should see comment "Good snippet!"' do + page.should have_content "Good snippet!" + end + + And 'I visit snippet page "Snippet one"' do + visit project_snippet_path(project, project_snippet) + end + + def project + @project ||= Project.find_by_name!("Shop") + end + + def project_snippet + @project_snippet ||= ProjectSnippet.find_by_title!("Snippet One") + end +end diff --git a/features/steps/shared/paths.rb b/features/steps/shared/paths.rb index 628a179ae9d..21b6159bce3 100644 --- a/features/steps/shared/paths.rb +++ b/features/steps/shared/paths.rb @@ -149,7 +149,7 @@ module SharedPaths # Stub Graph max_size to speed up test (10 commits vs. 650) Network::Graph.stub(max_count: 10) - visit project_graph_path(@project, root_ref) + visit project_network_path(@project, root_ref) end step "I visit my project's issues page" do @@ -275,6 +275,22 @@ module SharedPaths visit public_root_path end + # ---------------------------------------- + # Snippets + # ---------------------------------------- + + Given 'I visit project "Shop" snippets page' do + visit project_snippets_path(project) + end + + Given 'I visit snippets page' do + visit snippets_path + end + + Given 'I visit new snippet page' do + visit new_snippet_path + end + def root_ref @project.repository.root_ref end diff --git a/features/steps/shared/snippet.rb b/features/steps/shared/snippet.rb new file mode 100644 index 00000000000..543e43196a5 --- /dev/null +++ b/features/steps/shared/snippet.rb @@ -0,0 +1,21 @@ +module SharedSnippet + include Spinach::DSL + + And 'I have public "Personal snippet one" snippet' do + create(:personal_snippet, + title: "Personal snippet one", + content: "Test content", + file_name: "snippet.rb", + private: false, + author: current_user) + end + + And 'I have private "Personal snippet private" snippet' do + create(:personal_snippet, + title: "Personal snippet private", + content: "Provate content", + file_name: "private_snippet.rb", + private: true, + author: current_user) + end +end diff --git a/features/steps/snippets/discover_snippets.rb b/features/steps/snippets/discover_snippets.rb new file mode 100644 index 00000000000..3afe019adf6 --- /dev/null +++ b/features/steps/snippets/discover_snippets.rb @@ -0,0 +1,17 @@ +class DiscoverSnippets < Spinach::FeatureSteps + include SharedAuthentication + include SharedPaths + include SharedSnippet + + Then 'I should see "Personal snippet one" in snippets' do + page.should have_content "Personal snippet one" + end + + And 'I should not see "Personal snippet private" in snippets' do + page.should_not have_content "Personal snippet private" + end + + def snippet + @snippet ||= PersonalSnippet.find_by_title!("Personal snippet one") + end +end diff --git a/features/steps/snippets/snippets.rb b/features/steps/snippets/snippets.rb new file mode 100644 index 00000000000..1fac8d0f988 --- /dev/null +++ b/features/steps/snippets/snippets.rb @@ -0,0 +1,65 @@ +class SnippetsFeature < Spinach::FeatureSteps + include SharedAuthentication + include SharedPaths + include SharedProject + include SharedSnippet + + Given 'I click link "Personal snippet one"' do + click_link "Personal snippet one" + end + + And 'I should not see "Personal snippet one" in snippets' do + page.should_not have_content "Personal snippet one" + end + + And 'I click link "Edit"' do + within ".file_title" do + click_link "Edit" + end + end + + And 'I click link "Destroy"' do + click_link "Destroy" + end + + And 'I submit new snippet "Personal snippet three"' do + fill_in "personal_snippet_title", :with => "Personal snippet three" + select "forever", :from => "personal_snippet_expires_at" + fill_in "personal_snippet_file_name", :with => "my_snippet.rb" + within('.file-editor') do + find(:xpath, "//input[@id='personal_snippet_content']").set 'Content of snippet three' + end + click_button "Save" + end + + Then 'I should see snippet "Personal snippet three"' do + page.should have_content "Personal snippet three" + page.should have_content "Content of snippet three" + end + + And 'I submit new title "Personal snippet new title"' do + fill_in "personal_snippet_title", :with => "Personal snippet new title" + click_button "Save" + end + + Then 'I should see "Personal snippet new title"' do + page.should have_content "Personal snippet new title" + end + + And 'I uncheck "Private" checkbox' do + find(:xpath, "//input[@id='personal_snippet_private']").set true + click_button "Save" + end + + Then 'I should see "Personal snippet one" public' do + page.should have_no_xpath("//i[@class='public-snippet']") + end + + And 'I visit snippet page "Personal snippet one"' do + visit snippet_path(snippet) + end + + def snippet + @snippet ||= PersonalSnippet.find_by_title!("Personal snippet one") + end +end diff --git a/features/steps/snippets/user_snippets.rb b/features/steps/snippets/user_snippets.rb new file mode 100644 index 00000000000..15d6da6db3d --- /dev/null +++ b/features/steps/snippets/user_snippets.rb @@ -0,0 +1,41 @@ +class UserSnippets < Spinach::FeatureSteps + include SharedAuthentication + include SharedPaths + include SharedSnippet + + Given 'I visit my snippets page' do + visit user_snippets_path(current_user) + end + + Then 'I should see "Personal snippet one" in snippets' do + page.should have_content "Personal snippet one" + end + + And 'I should see "Personal snippet private" in snippets' do + page.should have_content "Personal snippet private" + end + + Then 'I should not see "Personal snippet one" in snippets' do + page.should_not have_content "Personal snippet one" + end + + And 'I should not see "Personal snippet private" in snippets' do + page.should_not have_content "Personal snippet private" + end + + Given 'I click "Public" filter' do + within('.nav-stacked') do + click_link "Public" + end + end + + Given 'I click "Private" filter' do + within('.nav-stacked') do + click_link "Private" + end + end + + def snippet + @snippet ||= PersonalSnippet.find_by_title!("Personal snippet one") + end +end diff --git a/lib/api/api.rb b/lib/api/api.rb index 9571d49d9d5..5d97d50cb82 100644 --- a/lib/api/api.rb +++ b/lib/api/api.rb @@ -36,5 +36,8 @@ module API mount Internal mount SystemHooks mount UserTeams + mount ProjectSnippets + mount DeployKeys + mount ProjectHooks end end diff --git a/lib/api/deploy_keys.rb b/lib/api/deploy_keys.rb new file mode 100644 index 00000000000..55c947eb176 --- /dev/null +++ b/lib/api/deploy_keys.rb @@ -0,0 +1,84 @@ +module API + # Projects API + class DeployKeys < Grape::API + before { authenticate! } + + resource :projects do + helpers do + def handle_project_member_errors(errors) + if errors[:project_access].any? + error!(errors[:project_access], 422) + end + not_found! + end + end + + + # Get a specific project's keys + # + # Example Request: + # GET /projects/:id/keys + get ":id/keys" do + present user_project.deploy_keys, with: Entities::SSHKey + end + + # Get single key owned by currently authenticated user + # + # Example Request: + # GET /projects/:id/keys/:id + get ":id/keys/:key_id" do + key = user_project.deploy_keys.find params[:key_id] + present key, with: Entities::SSHKey + end + + # Add new ssh key to currently authenticated user + # If deploy key already exists - it will be joined to project + # but only if original one was is accessible by same user + # + # Parameters: + # key (required) - New SSH Key + # title (required) - New SSH Key's title + # Example Request: + # POST /projects/:id/keys + post ":id/keys" do + attrs = attributes_for_keys [:title, :key] + + if attrs[:key].present? + attrs[:key].strip! + + # check if key already exist in project + key = user_project.deploy_keys.find_by_key(attrs[:key]) + if key + present key, with: Entities::SSHKey + return + end + + # Check for available deploy keys in other projects + key = current_user.accessible_deploy_keys.find_by_key(attrs[:key]) + if key + user_project.deploy_keys << key + present key, with: Entities::SSHKey + return + end + end + + key = DeployKey.new attrs + + if key.valid? && user_project.deploy_keys << key + present key, with: Entities::SSHKey + else + not_found! + end + end + + # Delete existed ssh key of currently authenticated user + # + # Example Request: + # DELETE /projects/:id/keys/:id + delete ":id/keys/:key_id" do + key = user_project.deploy_keys.find params[:key_id] + key.destroy + end + end + end +end diff --git a/lib/api/entities.rb b/lib/api/entities.rb index 025f44fb319..0d8cac5c8fd 100644 --- a/lib/api/entities.rb +++ b/lib/api/entities.rb @@ -26,11 +26,11 @@ module API end class Project < Grape::Entity - expose :id, :name, :description, :default_branch + expose :id, :description, :default_branch, :public, :ssh_url_to_repo, :http_url_to_repo, :web_url expose :owner, using: Entities::UserBasic - expose :public + expose :name, :name_with_namespace expose :path, :path_with_namespace - expose :issues_enabled, :merge_requests_enabled, :wall_enabled, :wiki_enabled, :created_at + expose :issues_enabled, :merge_requests_enabled, :wall_enabled, :wiki_enabled, :created_at, :last_activity_at expose :namespace end @@ -120,5 +120,11 @@ module API expose :note expose :author, using: Entities::UserBasic end + + class Event < Grape::Entity + expose :title, :project_id, :action_name + expose :target_id, :target_type, :author_id + expose :data, :target_title + end end end diff --git a/lib/api/issues.rb b/lib/api/issues.rb index 53e2e8cfa35..a2983e197e7 100644 --- a/lib/api/issues.rb +++ b/lib/api/issues.rb @@ -70,7 +70,7 @@ module API # assignee_id (optional) - The ID of a user to assign issue # milestone_id (optional) - The ID of a milestone to assign issue # labels (optional) - The labels of an issue - # state (optional) - The state of an issue (close|reopen) + # state_event (optional) - The state event of an issue (close|reopen) # Example Request: # PUT /projects/:id/issues/:issue_id put ":id/issues/:issue_id" do diff --git a/lib/api/milestones.rb b/lib/api/milestones.rb index a25bbad1302..aee12e7dc40 100644 --- a/lib/api/milestones.rb +++ b/lib/api/milestones.rb @@ -60,7 +60,7 @@ module API # title (optional) - The title of a milestone # description (optional) - The description of a milestone # due_date (optional) - The due date of a milestone - # state (optional) - The status of the milestone (close|activate) + # state_event (optional) - The state event of the milestone (close|activate) # Example Request: # PUT /projects/:id/milestones/:milestone_id put ":id/milestones/:milestone_id" do diff --git a/lib/api/project_hooks.rb b/lib/api/project_hooks.rb new file mode 100644 index 00000000000..28501256795 --- /dev/null +++ b/lib/api/project_hooks.rb @@ -0,0 +1,108 @@ +module API + # Projects API + class ProjectHooks < Grape::API + before { authenticate! } + + resource :projects do + helpers do + def handle_project_member_errors(errors) + if errors[:project_access].any? + error!(errors[:project_access], 422) + end + not_found! + end + end + + # Get project hooks + # + # Parameters: + # id (required) - The ID of a project + # Example Request: + # GET /projects/:id/hooks + get ":id/hooks" do + authorize! :admin_project, user_project + @hooks = paginate user_project.hooks + present @hooks, with: Entities::Hook + end + + # Get a project hook + # + # Parameters: + # id (required) - The ID of a project + # hook_id (required) - The ID of a project hook + # Example Request: + # GET /projects/:id/hooks/:hook_id + get ":id/hooks/:hook_id" do + authorize! :admin_project, user_project + @hook = user_project.hooks.find(params[:hook_id]) + present @hook, with: Entities::Hook + end + + + # Add hook to project + # + # Parameters: + # id (required) - The ID of a project + # url (required) - The hook URL + # Example Request: + # POST /projects/:id/hooks + post ":id/hooks" do + authorize! :admin_project, user_project + required_attributes! [:url] + + @hook = user_project.hooks.new({"url" => params[:url]}) + if @hook.save + present @hook, with: Entities::Hook + else + if @hook.errors[:url].present? + error!("Invalid url given", 422) + end + not_found! + end + end + + # Update an existing project hook + # + # Parameters: + # id (required) - The ID of a project + # hook_id (required) - The ID of a project hook + # url (required) - The hook URL + # Example Request: + # PUT /projects/:id/hooks/:hook_id + put ":id/hooks/:hook_id" do + @hook = user_project.hooks.find(params[:hook_id]) + authorize! :admin_project, user_project + required_attributes! [:url] + + attrs = attributes_for_keys [:url] + if @hook.update_attributes attrs + present @hook, with: Entities::Hook + else + if @hook.errors[:url].present? + error!("Invalid url given", 422) + end + not_found! + end + end + + # Deletes project hook. This is an idempotent function. + # + # Parameters: + # id (required) - The ID of a project + # hook_id (required) - The ID of hook to delete + # Example Request: + # DELETE /projects/:id/hooks/:hook_id + delete ":id/hooks/:hook_id" do + authorize! :admin_project, user_project + required_attributes! [:hook_id] + + begin + @hook = ProjectHook.find(params[:hook_id]) + @hook.destroy + rescue + # ProjectHook can raise Error if hook_id not found + end + end + end + end +end diff --git a/lib/api/project_snippets.rb b/lib/api/project_snippets.rb new file mode 100644 index 00000000000..bee6544ea3d --- /dev/null +++ b/lib/api/project_snippets.rb @@ -0,0 +1,123 @@ +module API + # Projects API + class ProjectSnippets < Grape::API + before { authenticate! } + + resource :projects do + helpers do + def handle_project_member_errors(errors) + if errors[:project_access].any? + error!(errors[:project_access], 422) + end + not_found! + end + end + + # Get a project snippets + # + # Parameters: + # id (required) - The ID of a project + # Example Request: + # GET /projects/:id/snippets + get ":id/snippets" do + present paginate(user_project.snippets), with: Entities::ProjectSnippet + end + + # Get a project snippet + # + # Parameters: + # id (required) - The ID of a project + # snippet_id (required) - The ID of a project snippet + # Example Request: + # GET /projects/:id/snippets/:snippet_id + get ":id/snippets/:snippet_id" do + @snippet = user_project.snippets.find(params[:snippet_id]) + present @snippet, with: Entities::ProjectSnippet + end + + # Create a new project snippet + # + # Parameters: + # id (required) - The ID of a project + # title (required) - The title of a snippet + # file_name (required) - The name of a snippet file + # lifetime (optional) - The expiration date of a snippet + # code (required) - The content of a snippet + # Example Request: + # POST /projects/:id/snippets + post ":id/snippets" do + authorize! :write_project_snippet, user_project + required_attributes! [:title, :file_name, :code] + + attrs = attributes_for_keys [:title, :file_name] + attrs[:expires_at] = params[:lifetime] if params[:lifetime].present? + attrs[:content] = params[:code] if params[:code].present? + @snippet = user_project.snippets.new attrs + @snippet.author = current_user + + if @snippet.save + present @snippet, with: Entities::ProjectSnippet + else + not_found! + end + end + + # Update an existing project snippet + # + # Parameters: + # id (required) - The ID of a project + # snippet_id (required) - The ID of a project snippet + # title (optional) - The title of a snippet + # file_name (optional) - The name of a snippet file + # lifetime (optional) - The expiration date of a snippet + # code (optional) - The content of a snippet + # Example Request: + # PUT /projects/:id/snippets/:snippet_id + put ":id/snippets/:snippet_id" do + @snippet = user_project.snippets.find(params[:snippet_id]) + authorize! :modify_project_snippet, @snippet + + attrs = attributes_for_keys [:title, :file_name] + attrs[:expires_at] = params[:lifetime] if params[:lifetime].present? + attrs[:content] = params[:code] if params[:code].present? + + if @snippet.update_attributes attrs + present @snippet, with: Entities::ProjectSnippet + else + not_found! + end + end + + # Delete a project snippet + # + # Parameters: + # id (required) - The ID of a project + # snippet_id (required) - The ID of a project snippet + # Example Request: + # DELETE /projects/:id/snippets/:snippet_id + delete ":id/snippets/:snippet_id" do + begin + @snippet = user_project.snippets.find(params[:snippet_id]) + authorize! :modify_project_snippet, @snippet + @snippet.destroy + rescue + end + end + + # Get a raw project snippet + # + # Parameters: + # id (required) - The ID of a project + # snippet_id (required) - The ID of a project snippet + # Example Request: + # GET /projects/:id/snippets/:snippet_id/raw + get ":id/snippets/:snippet_id/raw" do + @snippet = user_project.snippets.find(params[:snippet_id]) + + env['api.format'] = :txt + content_type 'text/plain' + present @snippet.content + end + end + end +end diff --git a/lib/api/projects.rb b/lib/api/projects.rb index ddc403c12db..6dc051e4ba2 100644 --- a/lib/api/projects.rb +++ b/lib/api/projects.rb @@ -22,6 +22,15 @@ module API present @projects, with: Entities::Project end + # Get an owned projects list for authenticated user + # + # Example Request: + # GET /projects/owned + get '/owned' do + @projects = paginate current_user.owned_projects + present @projects, with: Entities::Project + end + # Get a single project # # Parameters: @@ -32,6 +41,20 @@ module API present user_project, with: Entities::Project end + # Get a single project events + # + # Parameters: + # id (required) - The ID of a project + # Example Request: + # GET /projects/:id + get ":id/events" do + limit = (params[:per_page] || 20).to_i + offset = (params[:page] || 0).to_i * limit + events = user_project.events.recent.limit(limit).offset(offset) + + present events, with: Entities::Event + end + # Create new project # # Parameters: @@ -194,244 +217,6 @@ module API {message: "Access revoked", id: params[:user_id].to_i} end end - - # Get project hooks - # - # Parameters: - # id (required) - The ID of a project - # Example Request: - # GET /projects/:id/hooks - get ":id/hooks" do - authorize! :admin_project, user_project - @hooks = paginate user_project.hooks - present @hooks, with: Entities::Hook - end - - # Get a project hook - # - # Parameters: - # id (required) - The ID of a project - # hook_id (required) - The ID of a project hook - # Example Request: - # GET /projects/:id/hooks/:hook_id - get ":id/hooks/:hook_id" do - authorize! :admin_project, user_project - @hook = user_project.hooks.find(params[:hook_id]) - present @hook, with: Entities::Hook - end - - - # Add hook to project - # - # Parameters: - # id (required) - The ID of a project - # url (required) - The hook URL - # Example Request: - # POST /projects/:id/hooks - post ":id/hooks" do - authorize! :admin_project, user_project - required_attributes! [:url] - - @hook = user_project.hooks.new({"url" => params[:url]}) - if @hook.save - present @hook, with: Entities::Hook - else - if @hook.errors[:url].present? - error!("Invalid url given", 422) - end - not_found! - end - end - - # Update an existing project hook - # - # Parameters: - # id (required) - The ID of a project - # hook_id (required) - The ID of a project hook - # url (required) - The hook URL - # Example Request: - # PUT /projects/:id/hooks/:hook_id - put ":id/hooks/:hook_id" do - @hook = user_project.hooks.find(params[:hook_id]) - authorize! :admin_project, user_project - required_attributes! [:url] - - attrs = attributes_for_keys [:url] - if @hook.update_attributes attrs - present @hook, with: Entities::Hook - else - if @hook.errors[:url].present? - error!("Invalid url given", 422) - end - not_found! - end - end - - # Deletes project hook. This is an idempotent function. - # - # Parameters: - # id (required) - The ID of a project - # hook_id (required) - The ID of hook to delete - # Example Request: - # DELETE /projects/:id/hooks/:hook_id - delete ":id/hooks/:hook_id" do - authorize! :admin_project, user_project - required_attributes! [:hook_id] - - begin - @hook = ProjectHook.find(params[:hook_id]) - @hook.destroy - rescue - # ProjectHook can raise Error if hook_id not found - end - end - - # Get a project snippets - # - # Parameters: - # id (required) - The ID of a project - # Example Request: - # GET /projects/:id/snippets - get ":id/snippets" do - present paginate(user_project.snippets), with: Entities::ProjectSnippet - end - - # Get a project snippet - # - # Parameters: - # id (required) - The ID of a project - # snippet_id (required) - The ID of a project snippet - # Example Request: - # GET /projects/:id/snippets/:snippet_id - get ":id/snippets/:snippet_id" do - @snippet = user_project.snippets.find(params[:snippet_id]) - present @snippet, with: Entities::ProjectSnippet - end - - # Create a new project snippet - # - # Parameters: - # id (required) - The ID of a project - # title (required) - The title of a snippet - # file_name (required) - The name of a snippet file - # lifetime (optional) - The expiration date of a snippet - # code (required) - The content of a snippet - # Example Request: - # POST /projects/:id/snippets - post ":id/snippets" do - authorize! :write_snippet, user_project - required_attributes! [:title, :file_name, :code] - - attrs = attributes_for_keys [:title, :file_name] - attrs[:expires_at] = params[:lifetime] if params[:lifetime].present? - attrs[:content] = params[:code] if params[:code].present? - @snippet = user_project.snippets.new attrs - @snippet.author = current_user - - if @snippet.save - present @snippet, with: Entities::ProjectSnippet - else - not_found! - end - end - - # Update an existing project snippet - # - # Parameters: - # id (required) - The ID of a project - # snippet_id (required) - The ID of a project snippet - # title (optional) - The title of a snippet - # file_name (optional) - The name of a snippet file - # lifetime (optional) - The expiration date of a snippet - # code (optional) - The content of a snippet - # Example Request: - # PUT /projects/:id/snippets/:snippet_id - put ":id/snippets/:snippet_id" do - @snippet = user_project.snippets.find(params[:snippet_id]) - authorize! :modify_snippet, @snippet - - attrs = attributes_for_keys [:title, :file_name] - attrs[:expires_at] = params[:lifetime] if params[:lifetime].present? - attrs[:content] = params[:code] if params[:code].present? - - if @snippet.update_attributes attrs - present @snippet, with: Entities::ProjectSnippet - else - not_found! - end - end - - # Delete a project snippet - # - # Parameters: - # id (required) - The ID of a project - # snippet_id (required) - The ID of a project snippet - # Example Request: - # DELETE /projects/:id/snippets/:snippet_id - delete ":id/snippets/:snippet_id" do - begin - @snippet = user_project.snippets.find(params[:snippet_id]) - authorize! :modify_snippet, user_project - @snippet.destroy - rescue - end - end - - # Get a raw project snippet - # - # Parameters: - # id (required) - The ID of a project - # snippet_id (required) - The ID of a project snippet - # Example Request: - # GET /projects/:id/snippets/:snippet_id/raw - get ":id/snippets/:snippet_id/raw" do - @snippet = user_project.snippets.find(params[:snippet_id]) - content_type 'text/plain' - present @snippet.content - end - - # Get a specific project's keys - # - # Example Request: - # GET /projects/:id/keys - get ":id/keys" do - present user_project.deploy_keys, with: Entities::SSHKey - end - - # Get single key owned by currently authenticated user - # - # Example Request: - # GET /projects/:id/keys/:id - get ":id/keys/:key_id" do - key = user_project.deploy_keys.find params[:key_id] - present key, with: Entities::SSHKey - end - - # Add new ssh key to currently authenticated user - # - # Parameters: - # key (required) - New SSH Key - # title (required) - New SSH Key's title - # Example Request: - # POST /projects/:id/keys - post ":id/keys" do - attrs = attributes_for_keys [:title, :key] - key = DeployKey.new attrs - if key.valid? && user_project.deploy_keys << key - present key, with: Entities::SSHKey - else - not_found! - end - end - - # Delete existed ssh key of currently authenticated user - # - # Example Request: - # DELETE /projects/:id/keys/:id - delete ":id/keys/:key_id" do - key = user_project.deploy_keys.find params[:key_id] - key.destroy - end end end end diff --git a/lib/api/repositories.rb b/lib/api/repositories.rb index 2ed5ea20891..5db17b7e414 100644 --- a/lib/api/repositories.rb +++ b/lib/api/repositories.rb @@ -94,7 +94,7 @@ module API get ":id/repository/commits" do authorize! :download_code, user_project - page = params[:page] || 0 + page = (params[:page] || 0).to_i per_page = (params[:per_page] || 20).to_i ref = params[:ref_name] || user_project.try(:default_branch) || 'master' @@ -102,6 +102,31 @@ module API present commits, with: Entities::RepoCommit end + # Get a project repository tree + # + # Parameters: + # id (required) - The ID of a project + # ref_name (optional) - The name of a repository branch or tag, if not given the default branch is used + # Example Request: + # GET /projects/:id/repository/tree + get ":id/repository/tree" do + authorize! :download_code, user_project + + ref = params[:ref_name] || user_project.try(:default_branch) || 'master' + path = params[:path] || nil + + commit = user_project.repository.commit(ref) + tree = Tree.new(user_project.repository, commit.id, ref, path) + + trees = [] + + %w(trees blobs submodules).each do |type| + trees += tree.send(type).map { |t| { name: t.name, type: type.singularize, mode: t.mode, id: t.id } } + end + + trees + end + # Get a raw file contents # # Parameters: diff --git a/lib/backup/repository.rb b/lib/backup/repository.rb index 62a510f2acc..c5e3d049fd7 100644 --- a/lib/backup/repository.rb +++ b/lib/backup/repository.rb @@ -71,7 +71,7 @@ module Backup print 'Put GitLab hooks in repositories dirs'.yellow gitlab_shell_user_home = File.expand_path("~#{Gitlab.config.gitlab_shell.ssh_user}") - if system("#{gitlab_shell_user_home}/gitlab-shell/support/rewrite-hooks.sh") + if system("#{gitlab_shell_user_home}/gitlab-shell/support/rewrite-hooks.sh #{Gitlab.config.gitlab_shell.repos_path}") puts " [DONE]".green else puts " [FAILED]".red diff --git a/lib/gitlab/user_team_manager.rb b/lib/gitlab/user_team_manager.rb index c9ca0f3c5bc..d5ec4ff6676 100644 --- a/lib/gitlab/user_team_manager.rb +++ b/lib/gitlab/user_team_manager.rb @@ -99,8 +99,8 @@ module Gitlab teams ||= project.user_teams.with_member(user) - if action && (action == :added) && (teams.count == 1) - result_access ||= project.users_project.with_user(user).first.project_access + if action && (action == :added) + result_access = project.users_projects.with_user(user).first.project_access if project.users_projects.with_user(user).any? end if teams.any? diff --git a/lib/gitlab/version_info.rb b/lib/gitlab/version_info.rb index 31b72720972..6ee41e85cc9 100644 --- a/lib/gitlab/version_info.rb +++ b/lib/gitlab/version_info.rb @@ -5,7 +5,7 @@ module Gitlab attr_reader :major, :minor, :patch def self.parse(str) - if m = str.match(/(\d+)\.(\d+)\.(\d+)/) + if str && m = str.match(/(\d+)\.(\d+)\.(\d+)/) VersionInfo.new(m[1].to_i, m[2].to_i, m[3].to_i) else VersionInfo.new diff --git a/lib/redcarpet/render/gitlab_html.rb b/lib/redcarpet/render/gitlab_html.rb index 4f2c86e2d41..318adbf1894 100644 --- a/lib/redcarpet/render/gitlab_html.rb +++ b/lib/redcarpet/render/gitlab_html.rb @@ -11,7 +11,8 @@ class Redcarpet::Render::GitlabHTML < Redcarpet::Render::HTML def block_code(code, language) options = { options: {encoding: 'utf-8'} } - options.merge!(lexer: language.downcase) if Pygments::Lexer.find(language) + lexer = Pygments::Lexer.find(language) # language can be an alias + options.merge!(lexer: lexer.name.downcase) if lexer # downcase is required # New lines are placed to fix an rendering issue # with code wrapped inside <h1> tag for next case: diff --git a/lib/support/nginx/gitlab b/lib/support/nginx/gitlab index b2500659bcd..be7a378b3aa 100644 --- a/lib/support/nginx/gitlab +++ b/lib/support/nginx/gitlab @@ -9,6 +9,7 @@ upstream gitlab { server { listen YOUR_SERVER_IP:80 default_server; # e.g., listen 192.168.1.1:80; In most cases *:80 is a good idea server_name YOUR_SERVER_FQDN; # e.g., server_name source.example.com; + server_tokens off; # don't show the version number, a security best practice root /home/git/gitlab/public; # individual nginx logs for this gitlab vhost diff --git a/lib/tasks/gitlab/backup.rake b/lib/tasks/gitlab/backup.rake index 65d99d1aea3..d071938acb5 100644 --- a/lib/tasks/gitlab/backup.rake +++ b/lib/tasks/gitlab/backup.rake @@ -90,13 +90,21 @@ namespace :gitlab do settings = YAML.load_file("backup_information.yml") ENV["VERSION"] = "#{settings[:db_version]}" if settings[:db_version].to_i > 0 - # restoring mismatching backups can lead to unexpected problems - if settings[:gitlab_version] != %x{git rev-parse HEAD}.gsub(/\n/,"") - puts "GitLab version mismatch:".red - puts " Your current HEAD differs from the HEAD in the backup!".red - puts " Please switch to the following revision and try again:".red - puts " revision: #{settings[:gitlab_version]}".red - exit 1 + # backups directory is not always sub of Rails root and able to execute the git rev-parse below + begin + Dir.chdir(Rails.root) + + # restoring mismatching backups can lead to unexpected problems + if settings[:gitlab_version] != %x{git rev-parse HEAD}.gsub(/\n/, "") + puts "GitLab version mismatch:".red + puts " Your current HEAD differs from the HEAD in the backup!".red + puts " Please switch to the following revision and try again:".red + puts " revision: #{settings[:gitlab_version]}".red + exit 1 + end + ensure + # chdir back to original intended dir + Dir.chdir(Gitlab.config.backup.path) end Rake::Task["gitlab:backup:db:restore"].invoke diff --git a/lib/tasks/gitlab/check.rake b/lib/tasks/gitlab/check.rake index 59c2449444a..3d96eab0149 100644 --- a/lib/tasks/gitlab/check.rake +++ b/lib/tasks/gitlab/check.rake @@ -138,13 +138,15 @@ namespace :gitlab do def check_init_script_up_to_date print "Init script up-to-date? ... " + recipe_path = Rails.root.join("lib/support/init.d/", "gitlab") script_path = "/etc/init.d/gitlab" + unless File.exists?(script_path) puts "can't check because of previous errors".magenta return end - recipe_content = `curl https://raw.github.com/gitlabhq/gitlab-recipes/master/init.d/gitlab 2>/dev/null` + recipe_content = File.read(recipe_path) script_content = File.read(script_path) if recipe_content == script_content @@ -659,7 +661,7 @@ namespace :gitlab do current_version = Gitlab::VersionInfo.parse(gitlab_shell_version) print "GitLab Shell version >= #{required_version} ? ... " - if required_version <= current_version + if current_version.valid? && required_version <= current_version puts "OK (#{current_version})".green else puts "FAIL. Please update gitlab-shell to #{required_version} from #{current_version}".red @@ -673,7 +675,7 @@ namespace :gitlab do puts "Your git bin path is \"#{Gitlab.config.git.bin_path}\"" print "Git version >= #{required_version} ? ... " - if required_version <= current_version + if current_version.valid? && required_version <= current_version puts "yes (#{current_version})".green else puts "no".red diff --git a/lib/tasks/gitlab/shell.rake b/lib/tasks/gitlab/shell.rake index ec5451dd47c..11d4eacaa69 100644 --- a/lib/tasks/gitlab/shell.rake +++ b/lib/tasks/gitlab/shell.rake @@ -26,10 +26,12 @@ namespace :gitlab do warn_user_is_not_gitlab gitlab_shell_authorized_keys = File.join(File.expand_path("~#{Gitlab.config.gitlab_shell.ssh_user}"),'.ssh/authorized_keys') - puts "This will rebuild an authorized_keys file." - puts "You will lose any data stored in #{gitlab_shell_authorized_keys}." - ask_to_continue - puts "" + unless ENV['force'] == 'yes' + puts "This will rebuild an authorized_keys file." + puts "You will lose any data stored in #{gitlab_shell_authorized_keys}." + ask_to_continue + puts "" + end system("echo '# Managed by gitlab-shell' > #{gitlab_shell_authorized_keys}") diff --git a/script/check b/script/check index d2eb4a2f6d8..c907a98b5d9 100755 --- a/script/check +++ b/script/check @@ -1,2 +1,2 @@ #!/bin/sh -sudo -u gitlab -H bundle exec rake gitlab:check RAILS_ENV=production +sudo -u git -H bundle exec rake gitlab:check RAILS_ENV=production diff --git a/spec/factories.rb b/spec/factories.rb index f9e25382b61..b596f80fa9e 100644 --- a/spec/factories.rb +++ b/spec/factories.rb @@ -197,7 +197,7 @@ FactoryGirl.define do url end - factory :snippet do + factory :project_snippet do project author title @@ -205,6 +205,20 @@ FactoryGirl.define do file_name end + factory :personal_snippet do + author + title + content + file_name + end + + factory :snippet do + author + title + content + file_name + end + factory :protected_branch do name project diff --git a/spec/features/snippets_spec.rb b/spec/features/snippets_spec.rb deleted file mode 100644 index 1a0f6eaeef4..00000000000 --- a/spec/features/snippets_spec.rb +++ /dev/null @@ -1,99 +0,0 @@ -require 'spec_helper' - -describe "Snippets" do - let(:project) { create(:project) } - - before do - login_as :user - project.team << [@user, :developer] - end - - describe "GET /snippets" do - before do - @snippet = create(:snippet, - author: @user, - project: project) - - visit project_snippets_path(project) - end - - subject { page } - - it { should have_content(@snippet.title[0..10]) } - it { should have_content(@snippet.project.name) } - - describe "Destroy" do - before do - # admin access to remove snippet - @user.users_projects.destroy_all - project.team << [@user, :master] - visit edit_project_snippet_path(project, @snippet) - end - - it "should remove entry" do - expect { - click_link "destroy_snippet_#{@snippet.id}" - }.to change { Snippet.count }.by(-1) - end - end - end - - describe "New snippet" do - before do - visit project_snippets_path(project) - click_link "New Snippet" - end - - it "should open new snippet popup" do - page.current_path.should == new_project_snippet_path(project) - end - - describe "fill in", js: true do - before do - fill_in "snippet_title", with: "login function" - fill_in "snippet_file_name", with: "test.rb" - page.execute_script("editor.insert('def login; end');") - end - - it { expect { click_button "Save" }.to change {Snippet.count}.by(1) } - - it "should add new snippet to table" do - click_button "Save" - page.current_path.should == project_snippet_path(project, Snippet.last) - page.should have_content "login function" - page.should have_content "test.rb" - end - end - end - - describe "Edit snippet" do - before do - @snippet = create(:snippet, - author: @user, - project: project) - visit project_snippet_path(project, @snippet) - click_link "Edit Snippet" - end - - it "should open edit page" do - page.current_path.should == edit_project_snippet_path(project, @snippet) - end - - describe "fill in" do - before do - fill_in "snippet_title", with: "login function" - fill_in "snippet_file_name", with: "test.rb" - end - - it { expect { click_button "Save" }.to_not change {Snippet.count} } - - it "should update snippet fields" do - click_button "Save" - - page.current_path.should == project_snippet_path(project, @snippet) - page.should have_content "login function" - page.should have_content "test.rb" - end - end - end -end diff --git a/spec/helpers/application_helper_spec.rb b/spec/helpers/application_helper_spec.rb index ba1af08421b..229f49659cf 100644 --- a/spec/helpers/application_helper_spec.rb +++ b/spec/helpers/application_helper_spec.rb @@ -83,4 +83,26 @@ describe ApplicationHelper do end end + + describe "user_color_scheme_class" do + context "with current_user is nil" do + it "should return a string" do + stub!(:current_user).and_return(nil) + user_color_scheme_class.should be_kind_of(String) + end + end + + context "with a current_user" do + (1..5).each do |color_scheme_id| + context "with color_scheme_id == #{color_scheme_id}" do + it "should return a string" do + current_user = double(:color_scheme_id => color_scheme_id) + stub!(:current_user).and_return(current_user) + user_color_scheme_class.should be_kind_of(String) + end + end + end + end + end + end diff --git a/spec/helpers/gitlab_markdown_helper_spec.rb b/spec/helpers/gitlab_markdown_helper_spec.rb index 23b18fbf0eb..0f206f47234 100644 --- a/spec/helpers/gitlab_markdown_helper_spec.rb +++ b/spec/helpers/gitlab_markdown_helper_spec.rb @@ -10,7 +10,7 @@ describe GitlabMarkdownHelper do let(:commit) { project.repository.commit } let(:issue) { create(:issue, project: project) } let(:merge_request) { create(:merge_request, project: project) } - let(:snippet) { create(:snippet, project: project) } + let(:snippet) { create(:project_snippet, project: project) } let(:member) { project.users_projects.where(user_id: user).first } before do @@ -190,8 +190,43 @@ describe GitlabMarkdownHelper do describe "referencing a snippet" do let(:object) { snippet } let(:reference) { "$#{snippet.id}" } + let(:actual) { "Reference to #{reference}" } + let(:expected) { project_snippet_path(project, object) } + + it "should link using a valid id" do + gfm(actual).should match(expected) + end + + it "should link with adjacent text" do + # Wrap the reference in parenthesis + gfm(actual.gsub(reference, "(#{reference})")).should match(expected) + + # Append some text to the end of the reference + gfm(actual.gsub(reference, "#{reference}, right?")).should match(expected) + end + + it "should keep whitespace intact" do + actual = "Referenced #{reference} already." + expected = /Referenced <a.+>[^\s]+<\/a> already/ + gfm(actual).should match(expected) + end + + it "should not link with an invalid id" do + # Modify the reference string so it's still parsed, but is invalid + reference.gsub!(/^(.)(\d+)$/, '\1' + ('\2' * 2)) + gfm(actual).should == actual + end + + it "should include a title attribute" do + title = "Snippet: #{object.title}" + gfm(actual).should match(/title="#{title}"/) + end + + it "should include standard gfm classes" do + css = object.class.to_s.underscore + gfm(actual).should match(/class="\s?gfm gfm-snippet\s?"/) + end - include_examples 'referenced object' end describe "referencing multiple objects" do diff --git a/spec/javascripts/helpers/.gitkeep b/spec/javascripts/helpers/.gitkeep new file mode 100644 index 00000000000..e69de29bb2d --- /dev/null +++ b/spec/javascripts/helpers/.gitkeep diff --git a/spec/javascripts/stat_graph_contributors_graph_spec.js b/spec/javascripts/stat_graph_contributors_graph_spec.js new file mode 100644 index 00000000000..8d2e2038a55 --- /dev/null +++ b/spec/javascripts/stat_graph_contributors_graph_spec.js @@ -0,0 +1,125 @@ +describe("ContributorsGraph", function () { + describe("#set_x_domain", function () { + it("set the x_domain", function () { + ContributorsGraph.set_x_domain(20) + expect(ContributorsGraph.prototype.x_domain).toEqual(20) + }) + }) + + describe("#set_y_domain", function () { + it("sets the y_domain", function () { + ContributorsGraph.set_y_domain([{commits: 30}]) + expect(ContributorsGraph.prototype.y_domain).toEqual([0, 30]) + }) + }) + + describe("#init_x_domain", function () { + it("sets the initial x_domain", function () { + ContributorsGraph.init_x_domain([{date: "2013-01-31"}, {date: "2012-01-31"}]) + expect(ContributorsGraph.prototype.x_domain).toEqual(["2012-01-31", "2013-01-31"]) + }) + }) + + describe("#init_y_domain", function () { + it("sets the initial y_domain", function () { + ContributorsGraph.init_y_domain([{commits: 30}]) + expect(ContributorsGraph.prototype.y_domain).toEqual([0, 30]) + }) + }) + + describe("#init_domain", function () { + it("calls init_x_domain and init_y_domain", function () { + spyOn(ContributorsGraph, "init_x_domain") + spyOn(ContributorsGraph, "init_y_domain") + ContributorsGraph.init_domain() + expect(ContributorsGraph.init_x_domain).toHaveBeenCalled() + expect(ContributorsGraph.init_y_domain).toHaveBeenCalled() + }) + }) + + describe("#set_dates", function () { + it("sets the dates", function () { + ContributorsGraph.set_dates("2013-12-01") + expect(ContributorsGraph.prototype.dates).toEqual("2013-12-01") + }) + }) + + describe("#set_x_domain", function () { + it("sets the instance's x domain using the prototype's x_domain", function () { + ContributorsGraph.prototype.x_domain = 20 + var instance = new ContributorsGraph() + instance.x = d3.time.scale().range([0, 100]).clamp(true) + spyOn(instance.x, 'domain') + instance.set_x_domain() + expect(instance.x.domain).toHaveBeenCalledWith(20) + }) + }) + + describe("#set_y_domain", function () { + it("sets the instance's y domain using the prototype's y_domain", function () { + ContributorsGraph.prototype.y_domain = 30 + var instance = new ContributorsGraph() + instance.y = d3.scale.linear().range([100, 0]).nice() + spyOn(instance.y, 'domain') + instance.set_y_domain() + expect(instance.y.domain).toHaveBeenCalledWith(30) + }) + }) + + describe("#set_domain", function () { + it("calls set_x_domain and set_y_domain", function () { + var instance = new ContributorsGraph() + spyOn(instance, 'set_x_domain') + spyOn(instance, 'set_y_domain') + instance.set_domain() + expect(instance.set_x_domain).toHaveBeenCalled() + expect(instance.set_y_domain).toHaveBeenCalled() + }) + }) + + describe("#set_data", function () { + it("sets the data", function () { + var instance = new ContributorsGraph() + instance.set_data("20") + expect(instance.data).toEqual("20") + }) + }) +}) + +describe("ContributorsMasterGraph", function () { + + describe("#process_dates", function () { + it("gets and parses dates", function () { + var graph = new ContributorsMasterGraph() + var data = 'random data here' + spyOn(graph, 'parse_dates') + spyOn(graph, 'get_dates').andReturn("get") + spyOn(ContributorsGraph,'set_dates').andCallThrough() + graph.process_dates(data) + expect(graph.parse_dates).toHaveBeenCalledWith(data) + expect(graph.get_dates).toHaveBeenCalledWith(data) + expect(ContributorsGraph.set_dates).toHaveBeenCalledWith("get") + }) + }) + + describe("#get_dates", function () { + it("plucks the date field from data collection", function () { + var graph = new ContributorsMasterGraph() + var data = [{date: "2013-01-01"}, {date: "2012-12-15"}] + expect(graph.get_dates(data)).toEqual(["2013-01-01", "2012-12-15"]) + }) + }) + + describe("#parse_dates", function () { + it("parses the dates", function () { + var graph = new ContributorsMasterGraph() + var parseDate = d3.time.format("%Y-%m-%d").parse + var data = [{date: "2013-01-01"}, {date: "2012-12-15"}] + var correct = [{date: parseDate(data[0].date)}, {date: parseDate(data[1].date)}] + graph.parse_dates(data) + expect(data).toEqual(correct) + }) + }) + + +}) diff --git a/spec/javascripts/stat_graph_contributors_util_spec.js b/spec/javascripts/stat_graph_contributors_util_spec.js new file mode 100644 index 00000000000..367f0af05f8 --- /dev/null +++ b/spec/javascripts/stat_graph_contributors_util_spec.js @@ -0,0 +1,200 @@ +describe("ContributorsStatGraphUtil", function () { + + describe("#parse_log", function () { + it("returns a correctly parsed log", function () { + var fake_log = [ + {author: "Karlo Soriano", date: "2013-05-09", additions: 471}, + {author: "Dmitriy Zaporozhets", date: "2013-05-08", additions: 6, deletions: 1}, + {author: "Dmitriy Zaporozhets", date: "2013-05-08", additions: 19, deletions: 3}, + {author: "Dmitriy Zaporozhets", date: "2013-05-08", additions: 29, deletions: 3}] + + var correct_parsed_log = { + total: [ + {date: "2013-05-09", additions: 471, deletions: 0, commits: 1}, + {date: "2013-05-08", additions: 54, deletions: 7, commits: 3}], + by_author: + [ + { + author: "Karlo Soriano", + "2013-05-09": {date: "2013-05-09", additions: 471, deletions: 0, commits: 1} + }, + { + author: "Dmitriy Zaporozhets", + "2013-05-08": {date: "2013-05-08", additions: 54, deletions: 7, commits: 3} + } + ] + } + expect(ContributorsStatGraphUtil.parse_log(fake_log)).toEqual(correct_parsed_log) + }) + }) + + describe("#store_data", function () { + + var fake_entry = {author: "Karlo Soriano", date: "2013-05-09", additions: 471} + var fake_total = {} + var fake_by_author = {} + + it("calls #store_commits", function () { + spyOn(ContributorsStatGraphUtil, 'store_commits') + ContributorsStatGraphUtil.store_data(fake_entry, fake_total, fake_by_author) + expect(ContributorsStatGraphUtil.store_commits).toHaveBeenCalled() + }) + + it("calls #store_additions", function () { + spyOn(ContributorsStatGraphUtil, 'store_additions') + ContributorsStatGraphUtil.store_data(fake_entry, fake_total, fake_by_author) + expect(ContributorsStatGraphUtil.store_additions).toHaveBeenCalled() + }) + + it("calls #store_deletions", function () { + spyOn(ContributorsStatGraphUtil, 'store_deletions') + ContributorsStatGraphUtil.store_data(fake_entry, fake_total, fake_by_author) + expect(ContributorsStatGraphUtil.store_deletions).toHaveBeenCalled() + }) + + }) + + describe("#store_commits", function () { + var fake_total = "fake_total" + var fake_by_author = "fake_by_author" + + it("calls #add twice with arguments fake_total and fake_by_author respectively", function () { + spyOn(ContributorsStatGraphUtil, 'add') + ContributorsStatGraphUtil.store_commits(fake_total, fake_by_author) + expect(ContributorsStatGraphUtil.add.argsForCall).toEqual([["fake_total", "commits", 1], ["fake_by_author", "commits", 1]]) + }) + }) + + describe("#add", function () { + it("adds 1 to current test_field in collection", function () { + var fake_collection = {test_field: 10} + ContributorsStatGraphUtil.add(fake_collection, "test_field", 1) + expect(fake_collection.test_field).toEqual(11) + }) + + it("inits and adds 1 if test_field in collection is not defined", function () { + var fake_collection = {} + ContributorsStatGraphUtil.add(fake_collection, "test_field", 1) + expect(fake_collection.test_field).toEqual(1) + }) + }) + + describe("#store_additions", function () { + var fake_entry = {additions: 10} + var fake_total= "fake_total" + var fake_by_author = "fake_by_author" + it("calls #add twice with arguments fake_total and fake_by_author respectively", function () { + spyOn(ContributorsStatGraphUtil, 'add') + ContributorsStatGraphUtil.store_additions(fake_entry, fake_total, fake_by_author) + expect(ContributorsStatGraphUtil.add.argsForCall).toEqual([["fake_total", "additions", 10], ["fake_by_author", "additions", 10]]) + }) + }) + + describe("#store_deletions", function () { + var fake_entry = {deletions: 10} + var fake_total= "fake_total" + var fake_by_author = "fake_by_author" + it("calls #add twice with arguments fake_total and fake_by_author respectively", function () { + spyOn(ContributorsStatGraphUtil, 'add') + ContributorsStatGraphUtil.store_deletions(fake_entry, fake_total, fake_by_author) + expect(ContributorsStatGraphUtil.add.argsForCall).toEqual([["fake_total", "deletions", 10], ["fake_by_author", "deletions", 10]]) + }) + }) + + describe("#add_date", function () { + it("adds a date field to the collection", function () { + var fake_date = "2013-10-02" + var fake_collection = {} + ContributorsStatGraphUtil.add_date(fake_date, fake_collection) + expect(fake_collection[fake_date].date).toEqual("2013-10-02") + }) + }) + + describe("#add_author", function () { + it("adds an author field to the collection", function () { + var fake_author = "Author" + var fake_collection = {} + ContributorsStatGraphUtil.add_author(fake_author, fake_collection) + expect(fake_collection[fake_author].author).toEqual("Author") + }) + }) + + describe("#get_total_data", function () { + it("returns the collection sorted via specified field", function () { + var fake_parsed_log = { + total: [{date: "2013-05-09", additions: 471, deletions: 0, commits: 1}, + {date: "2013-05-08", additions: 54, deletions: 7, commits: 3}], + by_author:[ + { + author: "Karlo Soriano", + "2013-05-09": {date: "2013-05-09", additions: 471, deletions: 0, commits: 1} + }, + { + author: "Dmitriy Zaporozhets", + "2013-05-08": {date: "2013-05-08", additions: 54, deletions: 7, commits: 3} + } + ]}; + var correct_total_data = [{date: "2013-05-08", commits: 3}, + {date: "2013-05-09", commits: 1}]; + expect(ContributorsStatGraphUtil.get_total_data(fake_parsed_log, "commits")).toEqual(correct_total_data) + }) + }) + + describe("#pick_field", function () { + it("returns the collection with only the specified field and date", function () { + var fake_parsed_log_total = [{date: "2013-05-09", additions: 471, deletions: 0, commits: 1}, + {date: "2013-05-08", additions: 54, deletions: 7, commits: 3}]; + ContributorsStatGraphUtil.pick_field(fake_parsed_log_total, "commits") + var correct_pick_field_data = [{date: "2013-05-09", commits: 1},{date: "2013-05-08", commits: 3}]; + expect(ContributorsStatGraphUtil.pick_field(fake_parsed_log_total, "commits")).toEqual(correct_pick_field_data) + }) + }) + + describe("#get_author_data", function () { + it("returns the log by author sorted by specified field", function () { + var fake_parsed_log = { + total: [{date: "2013-05-09", additions: 471, deletions: 0, commits: 1}, + {date: "2013-05-08", additions: 54, deletions: 7, commits: 3}], + by_author:[ + { + author: "Karlo Soriano", + "2013-05-09": {date: "2013-05-09", additions: 471, deletions: 0, commits: 1} + }, + { + author: "Dmitriy Zaporozhets", + "2013-05-08": {date: "2013-05-08", additions: 54, deletions: 7, commits: 3} + } + ]} + var correct_author_data = [{author:"Dmitriy Zaporozhets",dates:{"2013-05-08":3},deletions:7,additions:54,"commits":3}, + {author:"Karlo Soriano",dates:{"2013-05-09":1},deletions:0,additions:471,commits:1}] + expect(ContributorsStatGraphUtil.get_author_data(fake_parsed_log, "commits")).toEqual(correct_author_data) + }) + }) + + describe("#parse_log_entry", function () { + it("adds the corresponding info from the log entry to the author", function () { + var fake_log_entry = { author: "Karlo Soriano", + "2013-05-09": {date: "2013-05-09", additions: 471, deletions: 0, commits: 1} + } + var correct_parsed_log = {author:"Karlo Soriano",dates:{"2013-05-09":1},deletions:0,additions:471,commits:1} + expect(ContributorsStatGraphUtil.parse_log_entry(fake_log_entry, 'commits', null)).toEqual(correct_parsed_log) + }) + }) + + describe("#in_range", function () { + var date = "2013-05-09" + it("returns true if date_range is null", function () { + expect(ContributorsStatGraphUtil.in_range(date, null)).toEqual(true) + }) + it("returns true if date is in range", function () { + var date_range = [new Date("2013-01-01"), new Date("2013-12-12")] + expect(ContributorsStatGraphUtil.in_range(date, date_range)).toEqual(true) + }) + it("returns false if date is not in range", function () { + var date_range = [new Date("1999-12-01"), new Date("2000-12-01")] + expect(ContributorsStatGraphUtil.in_range(date, date_range)).toEqual(false) + }) + }) + + +})
\ No newline at end of file diff --git a/spec/javascripts/stat_graph_spec.js b/spec/javascripts/stat_graph_spec.js new file mode 100644 index 00000000000..b8881769ac1 --- /dev/null +++ b/spec/javascripts/stat_graph_spec.js @@ -0,0 +1,17 @@ +describe("StatGraph", function () { + + describe("#get_log", function () { + it("returns log", function () { + StatGraph.log = "test"; + expect(StatGraph.get_log()).toBe("test"); + }); + }); + + describe("#set_log", function () { + it("sets the log", function () { + StatGraph.set_log("test"); + expect(StatGraph.log).toBe("test"); + }) + }) + +});
\ No newline at end of file diff --git a/spec/javascripts/support/jasmine.yml b/spec/javascripts/support/jasmine.yml new file mode 100644 index 00000000000..9bfa261a356 --- /dev/null +++ b/spec/javascripts/support/jasmine.yml @@ -0,0 +1,76 @@ +# src_files +# +# Return an array of filepaths relative to src_dir to include before jasmine specs. +# Default: [] +# +# EXAMPLE: +# +# src_files: +# - lib/source1.js +# - lib/source2.js +# - dist/**/*.js +# +src_files: + - assets/application.js + +# stylesheets +# +# Return an array of stylesheet filepaths relative to src_dir to include before jasmine specs. +# Default: [] +# +# EXAMPLE: +# +# stylesheets: +# - css/style.css +# - stylesheets/*.css +# +stylesheets: + - stylesheets/**/*.css + +# helpers +# +# Return an array of filepaths relative to spec_dir to include before jasmine specs. +# Default: ["helpers/**/*.js"] +# +# EXAMPLE: +# +# helpers: +# - helpers/**/*.js +# +helpers: + - helpers/**/*.js + +# spec_files +# +# Return an array of filepaths relative to spec_dir to include. +# Default: ["**/*[sS]pec.js"] +# +# EXAMPLE: +# +# spec_files: +# - **/*[sS]pec.js +# +spec_files: + - '**/*[sS]pec.js' + +# src_dir +# +# Source directory path. Your src_files must be returned relative to this path. Will use root if left blank. +# Default: project root +# +# EXAMPLE: +# +# src_dir: public +# +src_dir: + +# spec_dir +# +# Spec directory path. Your spec_files must be returned relative to this path. +# Default: spec/javascripts +# +# EXAMPLE: +# +# spec_dir: spec/javascripts +# +spec_dir: spec/javascripts diff --git a/spec/javascripts/support/jasmine_helper.rb b/spec/javascripts/support/jasmine_helper.rb new file mode 100644 index 00000000000..986a4c16f3e --- /dev/null +++ b/spec/javascripts/support/jasmine_helper.rb @@ -0,0 +1,11 @@ +#Use this file to set/override Jasmine configuration options +#You can remove it if you don't need it. +#This file is loaded *after* jasmine.yml is interpreted. +# +#Example: using a different boot file. +#Jasmine.configure do |config| +# @config.boot_dir = '/absolute/path/to/boot_dir' +# @config.boot_files = lambda { ['/absolute/path/to/boot_dir/file.js'] } +#end +# + diff --git a/spec/lib/gitlab/user_team_manager_spec.rb b/spec/lib/gitlab/user_team_manager_spec.rb new file mode 100644 index 00000000000..2ee3587b367 --- /dev/null +++ b/spec/lib/gitlab/user_team_manager_spec.rb @@ -0,0 +1,28 @@ +require 'spec_helper' + +describe Gitlab::UserTeamManager do + before do + @user = create :user + @project = create :project, creator: @user + + @master = create :user + @developer = create :user + @reporter = create :user + + @project.team << [@master, :master] + @project.team << [@developer, :developer] + @project.team << [@reporter, :reporter] + + @team = create :user_team, owner: @user + + @team.add_members([@master.id, @developer.id, @reporter.id], UsersProject::DEVELOPER, false) + end + + it "should assign team to project with correct permissions result" do + @team.assign_to_project(@project, UsersProject::MASTER) + + @project.users_projects.find_by_user_id(@master).project_access.should == UsersProject::MASTER + @project.users_projects.find_by_user_id(@developer).project_access.should == UsersProject::DEVELOPER + @project.users_projects.find_by_user_id(@reporter).project_access.should == UsersProject::DEVELOPER + end +end diff --git a/spec/models/project_snippet_spec.rb b/spec/models/project_snippet_spec.rb new file mode 100644 index 00000000000..716fd81c91b --- /dev/null +++ b/spec/models/project_snippet_spec.rb @@ -0,0 +1,30 @@ +# == Schema Information +# +# Table name: snippets +# +# id :integer not null, primary key +# title :string(255) +# content :text +# author_id :integer not null +# project_id :integer not null +# created_at :datetime not null +# updated_at :datetime not null +# file_name :string(255) +# expires_at :datetime +# + +require 'spec_helper' + +describe ProjectSnippet do + describe "Associations" do + it { should belong_to(:project) } + end + + describe "Mass assignment" do + it { should_not allow_mass_assignment_of(:project_id) } + end + + describe "Validation" do + it { should validate_presence_of(:project) } + end +end diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb index 04b4ce1763e..2e3870b1b65 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -36,7 +36,7 @@ describe Project do it { should have_many(:milestones).dependent(:destroy) } it { should have_many(:users_projects).dependent(:destroy) } it { should have_many(:notes).dependent(:destroy) } - it { should have_many(:snippets).dependent(:destroy) } + it { should have_many(:snippets).class_name('ProjectSnippet').dependent(:destroy) } it { should have_many(:deploy_keys_projects).dependent(:destroy) } it { should have_many(:deploy_keys) } it { should have_many(:hooks).dependent(:destroy) } diff --git a/spec/models/snippet_spec.rb b/spec/models/snippet_spec.rb index e4d1934829f..52355c38f0c 100644 --- a/spec/models/snippet_spec.rb +++ b/spec/models/snippet_spec.rb @@ -17,19 +17,16 @@ require 'spec_helper' describe Snippet do describe "Associations" do - it { should belong_to(:project) } it { should belong_to(:author).class_name('User') } it { should have_many(:notes).dependent(:destroy) } end describe "Mass assignment" do it { should_not allow_mass_assignment_of(:author_id) } - it { should_not allow_mass_assignment_of(:project_id) } end describe "Validation" do it { should validate_presence_of(:author) } - it { should validate_presence_of(:project) } it { should validate_presence_of(:title) } it { should ensure_length_of(:title).is_within(0..255) } diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index 7559c4cc3a1..4dd2048ccad 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -41,6 +41,7 @@ require 'spec_helper' describe User do describe "Associations" do it { should have_one(:namespace) } + it { should have_many(:snippets).class_name('Snippet').dependent(:destroy) } it { should have_many(:users_projects).dependent(:destroy) } it { should have_many(:groups) } it { should have_many(:keys).dependent(:destroy) } @@ -105,11 +106,33 @@ describe User do ActiveRecord::Base.observers.enable(:user_observer) @user = create :user @project = create :project, namespace: @user.namespace + @project_2 = create :project # Grant MASTER access to the user + @project_3 = create :project # Grant DEVELOPER access to the user + + UsersProject.add_users_into_projects( + [@project_2.id], [@user.id], UsersProject::MASTER + ) + UsersProject.add_users_into_projects( + [@project_3.id], [@user.id], UsersProject::DEVELOPER + ) end it { @user.authorized_projects.should include(@project) } + it { @user.authorized_projects.should include(@project_2) } + it { @user.authorized_projects.should include(@project_3) } it { @user.owned_projects.should include(@project) } + it { @user.owned_projects.should_not include(@project_2) } + it { @user.owned_projects.should_not include(@project_3) } it { @user.personal_projects.should include(@project) } + it { @user.personal_projects.should_not include(@project_2) } + it { @user.personal_projects.should_not include(@project_3) } + + # master_projects doesn't check creator/namespace. + # In real case the users_projects relation will certainly be assigned + # when the project is created. + it { @user.master_projects.should_not include(@project) } + it { @user.master_projects.should include(@project_2) } + it { @user.master_projects.should_not include(@project_3) } end describe 'groups' do @@ -125,6 +148,23 @@ describe User do it { @user.owned_groups.should == [@group] } end + describe 'teams' do + before do + ActiveRecord::Base.observers.enable(:user_observer) + @admin = create :user, admin: true + @user1 = create :user + @user2 = create :user + @team = create :user_team, owner: @user1 + end + + it { @admin.authorized_teams.should == [@team] } + it { @user1.authorized_teams.should == [@team] } + it { @user2.authorized_teams.should be_empty } + it { @admin.should be_can(:manage_user_team, @team) } + it { @user1.should be_can(:manage_user_team, @team) } + it { @user2.should_not be_can(:manage_user_team, @team) } + end + describe 'namespaced' do before do ActiveRecord::Base.observers.enable(:user_observer) diff --git a/spec/requests/api/notes_spec.rb b/spec/requests/api/notes_spec.rb index 78d55a7b4ed..11296aea73e 100644 --- a/spec/requests/api/notes_spec.rb +++ b/spec/requests/api/notes_spec.rb @@ -7,7 +7,7 @@ describe API::API do let!(:project) { create(:project, namespace: user.namespace ) } let!(:issue) { create(:issue, project: project, author: user) } let!(:merge_request) { create(:merge_request, project: project, author: user) } - let!(:snippet) { create(:snippet, project: project, author: user) } + let!(:snippet) { create(:project_snippet, project: project, author: user) } let!(:issue_note) { create(:note, noteable: issue, project: project, author: user) } let!(:merge_request_note) { create(:note, noteable: merge_request, project: project, author: user) } let!(:snippet_note) { create(:note, noteable: snippet, project: project, author: user) } diff --git a/spec/requests/api/projects_spec.rb b/spec/requests/api/projects_spec.rb index de0631d5b70..31075149647 100644 --- a/spec/requests/api/projects_spec.rb +++ b/spec/requests/api/projects_spec.rb @@ -10,7 +10,7 @@ describe API::API do let(:admin) { create(:admin) } let!(:project) { create(:project_with_code, creator_id: user.id) } let!(:hook) { create(:project_hook, project: project, url: "http://example.com") } - let!(:snippet) { create(:snippet, author: user, project: project, title: 'example') } + let!(:snippet) { create(:project_snippet, author: user, project: project, title: 'example') } let!(:users_project) { create(:users_project, user: user, project: project, project_access: UsersProject::MASTER) } let!(:users_project2) { create(:users_project, user: user3, project: project, project_access: UsersProject::DEVELOPER) } @@ -173,6 +173,29 @@ describe API::API do end end + describe "GET /projects/:id/events" do + it "should return a project events" do + get api("/projects/#{project.id}/events", user) + response.status.should == 200 + json_event = json_response.first + + json_event['action_name'].should == 'joined' + json_event['project_id'].to_i.should == project.id + end + + it "should return a 404 error if not found" do + get api("/projects/42/events", user) + response.status.should == 404 + json_response['message'].should == '404 Not Found' + end + + it "should return a 404 error if user is not a member" do + other_user = create(:user) + get api("/projects/#{project.id}/events", other_user) + response.status.should == 404 + end + end + describe "GET /projects/:id/members" do it "should return project team members" do get api("/projects/#{project.id}/members", user) diff --git a/spec/requests/api/repositories_spec.rb b/spec/requests/api/repositories_spec.rb index 31176316af2..13e6627840e 100644 --- a/spec/requests/api/repositories_spec.rb +++ b/spec/requests/api/repositories_spec.rb @@ -111,6 +111,29 @@ describe API::API do end end + describe "GET /projects/:id/repository/tree" do + context "authorized user" do + before { project.team << [user2, :reporter] } + + it "should return project commits" do + get api("/projects/#{project.id}/repository/tree", user) + response.status.should == 200 + + json_response.should be_an Array + json_response.first['name'].should == 'app' + json_response.first['type'].should == 'tree' + json_response.first['mode'].should == '040000' + end + end + + context "unauthorized user" do + it "should not return project commits" do + get api("/projects/#{project.id}/repository/tree") + response.status.should == 401 + end + end + end + describe "GET /projects/:id/repository/commits/:sha/blob" do it "should get the raw file contents" do get api("/projects/#{project.id}/repository/commits/master/blob?filepath=README.md", user) diff --git a/spec/routing/project_routing_spec.rb b/spec/routing/project_routing_spec.rb index f20a1ca51a4..b2b20ef6b5d 100644 --- a/spec/routing/project_routing_spec.rb +++ b/spec/routing/project_routing_spec.rb @@ -201,7 +201,11 @@ describe RefsController, "routing" do it "to #logs_tree" do get("/gitlabhq/refs/stable/logs_tree").should route_to('refs#logs_tree', project_id: 'gitlabhq', id: 'stable') + get("/gitlabhq/refs/feature%2345/logs_tree").should route_to('refs#logs_tree', project_id: 'gitlabhq', id: 'feature#45') + get("/gitlabhq/refs/feature%2B45/logs_tree").should route_to('refs#logs_tree', project_id: 'gitlabhq', id: 'feature+45') get("/gitlabhq/refs/stable/logs_tree/foo/bar/baz").should route_to('refs#logs_tree', project_id: 'gitlabhq', id: 'stable', path: 'foo/bar/baz') + get("/gitlabhq/refs/feature%2345/logs_tree/foo/bar/baz").should route_to('refs#logs_tree', project_id: 'gitlabhq', id: 'feature#45', path: 'foo/bar/baz') + get("/gitlabhq/refs/feature%2B45/logs_tree/foo/bar/baz").should route_to('refs#logs_tree', project_id: 'gitlabhq', id: 'feature+45', path: 'foo/bar/baz') get("/gitlab/gitlabhq/refs/stable/logs_tree/files.scss").should route_to('refs#logs_tree', project_id: 'gitlab/gitlabhq', id: 'stable', path: 'files.scss') end end @@ -258,13 +262,37 @@ end # project_snippet GET /:project_id/snippets/:id(.:format) snippets#show # PUT /:project_id/snippets/:id(.:format) snippets#update # DELETE /:project_id/snippets/:id(.:format) snippets#destroy -describe SnippetsController, "routing" do +describe Project::SnippetsController, "routing" do it "to #raw" do - get("/gitlabhq/snippets/1/raw").should route_to('snippets#raw', project_id: 'gitlabhq', id: '1') + get("/gitlabhq/snippets/1/raw").should route_to('projects/snippets#raw', project_id: 'gitlabhq', id: '1') end - it_behaves_like "RESTful project resources" do - let(:controller) { 'snippets' } + it "to #index" do + get("/gitlabhq/snippets").should route_to("projects/snippets#index", project_id: 'gitlabhq') + end + + it "to #create" do + post("/gitlabhq/snippets").should route_to("projects/snippets#create", project_id: 'gitlabhq') + end + + it "to #new" do + get("/gitlabhq/snippets/new").should route_to("projects/snippets#new", project_id: 'gitlabhq') + end + + it "to #edit" do + get("/gitlabhq/snippets/1/edit").should route_to("projects/snippets#edit", project_id: 'gitlabhq', id: '1') + end + + it "to #show" do + get("/gitlabhq/snippets/1").should route_to("projects/snippets#show", project_id: 'gitlabhq', id: '1') + end + + it "to #update" do + put("/gitlabhq/snippets/1").should route_to("projects/snippets#update", project_id: 'gitlabhq', id: '1') + end + + it "to #destroy" do + delete("/gitlabhq/snippets/1").should route_to("projects/snippets#destroy", project_id: 'gitlabhq', id: '1') end end @@ -422,9 +450,15 @@ describe CompareController, "routing" do end end -describe GraphController, "routing" do +describe NetworkController, "routing" do + it "to #show" do + get("/gitlabhq/network/master").should route_to('network#show', project_id: 'gitlabhq', id: 'master') + get("/gitlabhq/network/master.json").should route_to('network#show', project_id: 'gitlabhq', id: 'master', format: "json") + end +end + +describe GraphsController, "routing" do it "to #show" do - get("/gitlabhq/graph/master").should route_to('graph#show', project_id: 'gitlabhq', id: 'master') - get("/gitlabhq/graph/master.json").should route_to('graph#show', project_id: 'gitlabhq', id: 'master', format: "json") + get("/gitlabhq/graphs/master").should route_to('graphs#show', project_id: 'gitlabhq', id: 'master') end end diff --git a/spec/routing/routing_spec.rb b/spec/routing/routing_spec.rb index b6135b4ca81..aa3952f74b6 100644 --- a/spec/routing/routing_spec.rb +++ b/spec/routing/routing_spec.rb @@ -19,6 +19,51 @@ describe "Mounted Apps", "routing" do end end +# snippets GET /snippets(.:format) snippets#index +# POST /snippets(.:format) snippets#create +# new_snippet GET /snippets/new(.:format) snippets#new +# edit_snippet GET /snippets/:id/edit(.:format) snippets#edit +# snippet GET /snippets/:id(.:format) snippets#show +# PUT /snippets/:id(.:format) snippets#update +# DELETE /snippets/:id(.:format) snippets#destroy +describe SnippetsController, "routing" do + it "to #user_index" do + get("/s/User").should route_to('snippets#user_index', username: 'User') + end + + it "to #raw" do + get("/snippets/1/raw").should route_to('snippets#raw', id: '1') + end + + it "to #index" do + get("/snippets").should route_to('snippets#index') + end + + it "to #create" do + post("/snippets").should route_to('snippets#create') + end + + it "to #new" do + get("/snippets/new").should route_to('snippets#new') + end + + it "to #edit" do + get("/snippets/1/edit").should route_to('snippets#edit', id: '1') + end + + it "to #show" do + get("/snippets/1").should route_to('snippets#show', id: '1') + end + + it "to #update" do + put("/snippets/1").should route_to('snippets#update', id: '1') + end + + it "to #destroy" do + delete("/snippets/1").should route_to('snippets#destroy', id: '1') + end +end + # help GET /help(.:format) help#index # help_permissions GET /help/permissions(.:format) help#permissions # help_workflow GET /help/workflow(.:format) help#workflow |
