OSDN Git Service

Merge pull request #4176 from Andrew8xx8/patch-2
authorDmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com>
Tue, 11 Jun 2013 13:44:18 +0000 (06:44 -0700)
committerDmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com>
Tue, 11 Jun 2013 13:44:18 +0000 (06:44 -0700)
500 error on showing not existed commit fixed

185 files changed:
CHANGELOG
Gemfile
Gemfile.lock
VERSION
app/assets/images/dark-scheme-preview.png [moved from app/assets/images/dark.png with 100% similarity]
app/assets/images/monokai-scheme-preview.png [moved from app/assets/images/monokai.png with 100% similarity]
app/assets/images/solarized-dark-scheme-preview.png [moved from app/assets/images/solarized_dark.png with 100% similarity]
app/assets/images/white-scheme-preview.png [moved from app/assets/images/white.png with 100% similarity]
app/assets/javascripts/admin.js.coffee
app/assets/javascripts/application.js
app/assets/javascripts/commits.js.coffee
app/assets/javascripts/stat_graph.js.coffee [new file with mode: 0644]
app/assets/javascripts/stat_graph_contributors.js.coffee [new file with mode: 0644]
app/assets/javascripts/stat_graph_contributors_graph.js.coffee [new file with mode: 0644]
app/assets/javascripts/stat_graph_contributors_util.js.coffee [new file with mode: 0644]
app/assets/javascripts/users_select.js.coffee
app/assets/stylesheets/application.scss
app/assets/stylesheets/common.scss
app/assets/stylesheets/highlight/dark.scss
app/assets/stylesheets/sections/stat_graph.scss [new file with mode: 0644]
app/assets/stylesheets/selects.scss
app/controllers/admin/groups_controller.rb
app/controllers/admin/projects_controller.rb
app/controllers/deploy_keys_controller.rb
app/controllers/graphs_controller.rb [new file with mode: 0644]
app/controllers/groups_controller.rb
app/controllers/help_controller.rb
app/controllers/network_controller.rb [moved from app/controllers/graph_controller.rb with 90% similarity]
app/controllers/projects/application_controller.rb
app/controllers/projects/snippets_controller.rb [new file with mode: 0644]
app/controllers/projects/teams_controller.rb
app/controllers/projects_controller.rb
app/controllers/refs_controller.rb
app/controllers/snippets_controller.rb
app/controllers/teams_controller.rb
app/controllers/users_controller.rb
app/helpers/application_helper.rb
app/helpers/events_helper.rb
app/helpers/namespaces_helper.rb
app/helpers/snippets_helper.rb
app/helpers/tab_helper.rb
app/helpers/tree_helper.rb
app/models/ability.rb
app/models/event.rb
app/models/network/graph.rb
app/models/note.rb
app/models/personal_snippet.rb [new file with mode: 0644]
app/models/project.rb
app/models/project_snippet.rb [new file with mode: 0644]
app/models/snippet.rb
app/models/user.rb
app/models/user_team.rb
app/observers/project_observer.rb
app/observers/user_observer.rb
app/views/admin/dashboard/index.html.haml
app/views/admin/groups/show.html.haml
app/views/admin/projects/index.html.haml
app/views/admin/teams/index.html.haml
app/views/admin/teams/projects/new.html.haml
app/views/admin/teams/show.html.haml
app/views/events/event/_note.html.haml
app/views/graphs/show.html.haml [new file with mode: 0644]
app/views/graphs/show.js.haml [new file with mode: 0644]
app/views/groups/_new_group_member.html.haml
app/views/groups/_new_member.html.haml
app/views/groups/edit.html.haml
app/views/groups/new.html.haml
app/views/groups/show.html.haml
app/views/help/_api_layout.html.haml [new file with mode: 0644]
app/views/help/api.html.haml
app/views/help/permissions.html.haml
app/views/issues/_form.html.haml
app/views/layouts/_head_panel.html.haml
app/views/layouts/nav/_dashboard.html.haml
app/views/layouts/nav/_project.html.haml
app/views/layouts/navless.html.haml [new file with mode: 0644]
app/views/layouts/public.html.haml
app/views/layouts/search.html.haml [new file with mode: 0644]
app/views/layouts/snippets.html.haml [new file with mode: 0644]
app/views/network/_head.html.haml [moved from app/views/graph/_head.html.haml with 86% similarity]
app/views/network/show.html.haml [moved from app/views/graph/show.html.haml with 84% similarity]
app/views/network/show.json.erb [moved from app/views/graph/show.json.erb with 100% similarity]
app/views/profiles/design.html.haml
app/views/projects/_clone_panel.html.haml
app/views/projects/fork.html.haml [new file with mode: 0644]
app/views/projects/new.html.haml
app/views/projects/show.html.haml
app/views/projects/snippets/_blob.html.haml [new file with mode: 0644]
app/views/projects/snippets/_form.html.haml [new file with mode: 0644]
app/views/projects/snippets/_snippet.html.haml [new file with mode: 0644]
app/views/projects/snippets/edit.html.haml [new file with mode: 0644]
app/views/projects/snippets/index.html.haml [new file with mode: 0644]
app/views/projects/snippets/new.html.haml [new file with mode: 0644]
app/views/projects/snippets/show.html.haml [new file with mode: 0644]
app/views/shared/_clone_panel.html.haml
app/views/snippets/_blob.html.haml
app/views/snippets/_form.html.haml
app/views/snippets/_snippet.html.haml
app/views/snippets/_snippets.html.haml [new file with mode: 0644]
app/views/snippets/current_user_index.html.haml [new file with mode: 0644]
app/views/snippets/edit.html.haml
app/views/snippets/index.html.haml
app/views/snippets/new.html.haml
app/views/snippets/show.html.haml
app/views/snippets/user_index.html.haml [new file with mode: 0644]
app/views/team_members/_team.html.haml
app/views/team_members/_team_member.html.haml
app/views/teams/new.html.haml
app/views/teams/show.html.haml
config/initializers/smtp_settings.rb.sample
config/routes.rb
db/migrate/20130323174317_add_private_to_snippets.rb [new file with mode: 0644]
db/migrate/20130324151736_add_type_to_snippets.rb [new file with mode: 0644]
db/migrate/20130324172327_change_project_id_to_null_in_snipepts.rb [new file with mode: 0644]
db/migrate/20130324203535_add_type_value_for_snippets.rb [new file with mode: 0644]
db/schema.rb
doc/api/README.md
doc/api/deploy_keys.md [new file with mode: 0644]
doc/api/issues.md
doc/api/milestones.md
doc/api/project_snippets.md [moved from doc/api/snippets.md with 100% similarity]
doc/api/projects.md
doc/api/repositories.md
doc/install/installation.md
doc/make_release.md [new file with mode: 0644]
doc/update/5.1-to-5.2.md
features/admin/groups.feature
features/dashboard/active_tab.feature
features/group/group.feature
features/project/graph.feature [new file with mode: 0644]
features/project/project.feature
features/project/snippets.feature [new file with mode: 0644]
features/snippets/discover_snippets.feature [new file with mode: 0644]
features/snippets/snippets.feature [new file with mode: 0644]
features/snippets/user_snippets.feature [new file with mode: 0644]
features/steps/admin/admin_groups.rb
features/steps/admin/admin_teams.rb
features/steps/dashboard/dashboard_active_tab.rb
features/steps/group/group.rb
features/steps/profile/profile.rb
features/steps/project/deploy_keys.rb
features/steps/project/project_graph.rb [new file with mode: 0644]
features/steps/project/project_network_graph.rb
features/steps/project/project_snippets.rb [new file with mode: 0644]
features/steps/shared/paths.rb
features/steps/shared/snippet.rb [new file with mode: 0644]
features/steps/snippets/discover_snippets.rb [new file with mode: 0644]
features/steps/snippets/snippets.rb [new file with mode: 0644]
features/steps/snippets/user_snippets.rb [new file with mode: 0644]
lib/api/api.rb
lib/api/deploy_keys.rb [new file with mode: 0644]
lib/api/entities.rb
lib/api/issues.rb
lib/api/milestones.rb
lib/api/project_hooks.rb [new file with mode: 0644]
lib/api/project_snippets.rb [new file with mode: 0644]
lib/api/projects.rb
lib/api/repositories.rb
lib/backup/repository.rb
lib/gitlab/version_info.rb
lib/redcarpet/render/gitlab_html.rb
lib/support/nginx/gitlab
lib/tasks/gitlab/backup.rake
lib/tasks/gitlab/check.rake
lib/tasks/gitlab/shell.rake
script/check
spec/factories.rb
spec/features/snippets_spec.rb [deleted file]
spec/helpers/application_helper_spec.rb
spec/helpers/gitlab_markdown_helper_spec.rb
spec/javascripts/helpers/.gitkeep [new file with mode: 0644]
spec/javascripts/stat_graph_contributors_graph_spec.js [new file with mode: 0644]
spec/javascripts/stat_graph_contributors_util_spec.js [new file with mode: 0644]
spec/javascripts/stat_graph_spec.js [new file with mode: 0644]
spec/javascripts/support/jasmine.yml [new file with mode: 0644]
spec/javascripts/support/jasmine_helper.rb [new file with mode: 0644]
spec/models/project_snippet_spec.rb [new file with mode: 0644]
spec/models/project_spec.rb
spec/models/snippet_spec.rb
spec/models/user_spec.rb
spec/requests/api/notes_spec.rb
spec/requests/api/projects_spec.rb
spec/requests/api/repositories_spec.rb
spec/routing/project_routing_spec.rb
spec/routing/routing_spec.rb

index 0416a51..f26466f 100644 (file)
--- 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
diff --git a/Gemfile b/Gemfile
index a596b99..e4206ab 100644 (file)
--- a/Gemfile
+++ b/Gemfile
@@ -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
index 0af3d51..b986539 100644 (file)
@@ -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
diff --git a/VERSION b/VERSION
index 9718a66..455f90e 100644 (file)
--- a/VERSION
+++ b/VERSION
@@ -1 +1 @@
-5.3.0.pre
+5.3.0.beta1
index c83b74a..da0077e 100644 (file)
@@ -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()
 
index ab5fc1b..0767b82 100644 (file)
@@ -27,3 +27,5 @@
 //= require branch-graph
 //= require ace-src-noconflict/ace
 //= require_tree .
+//= require d3
+//= require underscore
index ab8eaa6..de4c06a 100644 (file)
@@ -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 (file)
index 0000000..b129619
--- /dev/null
@@ -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 (file)
index 0000000..12dfe4d
--- /dev/null
@@ -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 (file)
index 0000000..e7a120f
--- /dev/null
@@ -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 (file)
index 0000000..8f81631
--- /dev/null
@@ -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
index f9e523e..8286ca2 100644 (file)
@@ -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
index 85e43ed..b1a2342 100644 (file)
@@ -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";
index ccc6f7a..8c76b4b 100644 (file)
@@ -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;
+}
index a56c98c..129d33d 100644 (file)
@@ -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 (file)
index 0000000..4baec34
--- /dev/null
@@ -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;
+}
index 7abbe80..9b7b6ad 100644 (file)
 
 .ajax-users-select {
   width: 400px;
+
+  &.input-large {
+    width: 210px;
+  }
 }
 
 .user-result {
index df520be..c38461c 100644 (file)
@@ -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
 
index bbb80cb..a63c469 100644 (file)
@@ -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
index c413c2f..35d28be 100644 (file)
@@ -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 (file)
index 0000000..5ae9c15
--- /dev/null
@@ -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
index 8976262..e6559b8 100644 (file)
@@ -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
index b22280d..2958367 100644 (file)
@@ -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
similarity index 90%
rename from app/controllers/graph_controller.rb
rename to app/controllers/network_controller.rb
index c79ed5c..3c8e747 100644 (file)
@@ -1,4 +1,4 @@
-class GraphController < ProjectResourceController
+class NetworkController < ProjectResourceController
   include ExtractsPath
   include ApplicationHelper
 
index 7e4776d..86e4a7c 100644 (file)
@@ -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 (file)
index 0000000..1165fa1
--- /dev/null
@@ -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
index 17e7367..c7d51b8 100644 (file)
@@ -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
index e202ed3..fad681e 100644 (file)
@@ -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
index e7def39..cae9193 100644 (file)
@@ -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
index a2e22a6..49b740a 100644 (file)
@@ -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
index 26c1d84..57ab2a8 100644 (file)
@@ -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
index e027057..4947c33 100644 (file)
@@ -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
index 4c0bbf8..663a414 100644 (file)
@@ -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
index 7155036..7c1aee7 100644 (file)
@@ -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
index a9a6c78..69ad3de 100644 (file)
@@ -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]} ]
index 6a91d61..b0abc2c 100644 (file)
@@ -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
index d2be4b1..19aba0f 100644 (file)
@@ -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"
index a8491df..af633f6 100644 (file)
@@ -48,7 +48,7 @@ module TreeHelper
   end
 
   def plain_text_readme? filename
-    filename == 'README'
+    filename =~ /^README(.txt)?$/i
   end
 
   # Simple shortcut to File.join
index 8f0a614..0b6314c 100644 (file)
@@ -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
           [
index 97f96ac..793ed44 100644 (file)
@@ -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
index ffec471..da815a8 100644 (file)
@@ -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
index 7b7e6e9..9a3481f 100644 (file)
@@ -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 (file)
index 0000000..d581c60
--- /dev/null
@@ -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
index 9147aed..8cb290f 100644 (file)
@@ -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
diff --git a/app/models/project_snippet.rb b/app/models/project_snippet.rb
new file mode 100644 (file)
index 0000000..a86f2e7
--- /dev/null
@@ -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
index c4ee35e..1b37ffe 100644 (file)
 #  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
     [
index c6dd086..3f51d7a 100644 (file)
@@ -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
index 364ea0d..a036ced 100644 (file)
@@ -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
index dda7be6..bd88bb8 100644 (file)
@@ -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,
index 6bb3c47..e969405 100644 (file)
@@ -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
index d3c938b..a69b260 100644 (file)
     %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
index 0e2e144..9c4b91b 100644 (file)
 %h3.page_title
   Group: #{@group.name}
 
-%br
-%table.zebra-striped
-  %thead
-    %tr
-      %th Group
-      %th
-  %tr
-    %td
-      %b
-        Name:
-    %td
-      = @group.name
-      &nbsp;
-      = 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"
index 59831d8..70ea460 100644 (file)
@@ -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"
index cf24ed5..05ddc3c 100644 (file)
@@ -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"
index b60dad3..dcb3dbb 100644 (file)
@@ -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
index bd4d90b..cf105c0 100644 (file)
@@ -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
-      &nbsp;
-      = 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"
-            &nbsp;
-            = 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
+              &nbsp;
+              = link_to 'Edit', edit_admin_team_member_path(@team, member), class: "btn btn-small"
+              &nbsp;
+              = 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"
-            &nbsp;
-            = 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)
+              &nbsp;
+              = link_to 'Edit', edit_admin_team_project_path(@team, project), class: "btn btn-small"
+              &nbsp;
+              = 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}"
index 8bcfa95..9dd0767 100644 (file)
@@ -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 (file)
index 0000000..05bc143
--- /dev/null
@@ -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 (file)
index 0000000..b7c9b41
--- /dev/null
@@ -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()
+  })
+
index 9cdbea6..78db1c2 100644 (file)
@@ -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
index b3424b0..4a762ed 100644 (file)
@@ -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
index 0f8c140..aaedcc5 100644 (file)
@@ -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"
index b395a8b..4aa58de 100644 (file)
@@ -1,5 +1,3 @@
-%h3.page_title New Group
-%hr
 = form_for @group do |f|
   - if @group.errors.any?
     .alert.alert-error
     .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
index 1ce008f..8afc4ab 100644 (file)
@@ -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 (file)
index 0000000..2f649a5
--- /dev/null
@@ -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
index 0c502ad..b6ad5e1 100644 (file)
-= 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"))
index 2075753..4748b29 100644 (file)
@@ -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
index 2bb5e6c..57460e6 100644 (file)
               %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
+                &nbsp;
+                = 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 ) {
index 2ea6c3e..f0b001f 100644 (file)
@@ -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
index 2ac3505..cae24c3 100644 (file)
@@ -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
 
index ec3da96..1c5781d 100644 (file)
@@ -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 (file)
index 0000000..7325664
--- /dev/null
@@ -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
index 435250b..e330da9 100644 (file)
@@ -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 (file)
index 0000000..177b3a4
--- /dev/null
@@ -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 (file)
index 0000000..e98aba4
--- /dev/null
@@ -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
similarity index 86%
rename from app/views/graph/_head.html.haml
rename to app/views/network/_head.html.haml
index 7a5b3c6..62ab8b0 100644 (file)
@@ -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
similarity index 84%
rename from app/views/graph/show.html.haml
rename to app/views/network/show.html.haml
index 0ee6648..a480cea 100644 (file)
@@ -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}'
index 878297f..9ada69c 100644 (file)
         %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
index f8276c3..ebce06e 100644 (file)
@@ -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 (file)
index 0000000..a1c109e
--- /dev/null
@@ -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
+      &ndash;
+      = @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
index e9099f2..0754ee8 100644 (file)
@@ -1,6 +1,4 @@
 .project-edit-container
-  %h3.page_title New Project
-  %hr
   .project-edit-errors
     = render 'projects/errors'
   .project-edit-content
index b2e9fd8..7d708ce 100644 (file)
@@ -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 (file)
index 0000000..e0d1669
--- /dev/null
@@ -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 (file)
index 0000000..99a8761
--- /dev/null
@@ -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 (file)
index 0000000..72865bf
--- /dev/null
@@ -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 (file)
index 0000000..e28b7d4
--- /dev/null
@@ -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 (file)
index 0000000..ce36bed
--- /dev/null
@@ -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 (file)
index 0000000..460af34
--- /dev/null
@@ -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 (file)
index 0000000..da93e4b
--- /dev/null
@@ -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"
index 62e0fa2..5120902 100644 (file)
@@ -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
index 017a33b..6f62ea0 100644 (file)
@@ -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}
index 993a200..95e9e03 100644 (file)
@@ -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
index a576500..194eb05 100644 (file)
@@ -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 (file)
index 0000000..636bf37
--- /dev/null
@@ -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 (file)
index 0000000..912f4c7
--- /dev/null
@@ -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'
+
index f81c0b8..1b88a85 100644 (file)
@@ -1 +1 @@
-= render "snippets/form"
+= render "snippets/form", url: snippet_path(@snippet)
index bacf23d..97f7b39 100644 (file)
@@ -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.
index f81c0b8..90e0a1f 100644 (file)
@@ -1 +1 @@
-= render "snippets/form"
+= render "snippets/form", url: snippets_path(@snippet)
index 12534ed..f425c4b 100644 (file)
@@ -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 (file)
index 0000000..79e3ddf
--- /dev/null
@@ -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'
index 4b49b30..4ff170a 100644 (file)
@@ -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
index 5fd8d24..d829a79 100644 (file)
@@ -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
index 99d3082..fe1f9cf 100644 (file)
@@ -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
     .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
index 2ad7f74..8582f85 100644 (file)
@@ -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
index 333c388..e62ad0f 100644 (file)
@@ -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
 #
index 3609ac0..6f72e2c 100644 (file)
@@ -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 (file)
index 0000000..92f3a5c
--- /dev/null
@@ -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 (file)
index 0000000..276aab2
--- /dev/null
@@ -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 (file)
index 0000000..4c992ba
--- /dev/null
@@ -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 (file)
index 0000000..8c05dd2
--- /dev/null
@@ -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
index 6a16caf..21e553d 100644 (file)
@@ -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"
index c3cfc0e..9120fe3 100644 (file)
@@ -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 (file)
index 0000000..5a39409
--- /dev/null
@@ -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
+
index a8ae740..8754759 100644 (file)
@@ -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**)
index 92a29ce..aa8f1bf 100644 (file)
@@ -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)
 
index d3d15ab..323c0be 100644 (file)
@@ -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
-
index 90fda38..7a9f766 100644 (file)
@@ -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
 
index 2b3e400..3b459ad 100644 (file)
@@ -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 (file)
index 0000000..24f8397
--- /dev/null
@@ -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
+
+* [![build status](http://ci.gitlab.org/projects/1/status.png?ref=master)](http://ci.gitlab.org/projects/1?ref=master) on ci.gitlab.org (master branch)
+
+* [![build status](https://secure.travis-ci.org/gitlabhq/gitlabhq.png)](https://travis-ci.org/gitlabhq/gitlabhq) on travis-ci.org (master branch)
+
+* [![Code Climate](https://codeclimate.com/github/gitlabhq/gitlabhq.png)](https://codeclimate.com/github/gitlabhq/gitlabhq)
+
+* [![Dependency Status](https://gemnasium.com/gitlabhq/gitlabhq.png)](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)
+
+* [![Coverage Status](https://coveralls.io/repos/gitlabhq/gitlabhq/badge.png?branch=master)](https://coveralls.io/r/gitlabhq/gitlabhq)
+
+## Make release branch
index 8f21168..3f87455 100644 (file)
@@ -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
+```
index 28f35e3..054dccf 100644 (file)
@@ -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"
index 6715ea2..3e1cf5a 100644 (file)
@@ -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
index a48affe..64424f4 100644 (file)
@@ -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 (file)
index 0000000..cda95f5
--- /dev/null
@@ -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
index 0c8c97c..59eda4a 100644 (file)
@@ -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 (file)
index 0000000..a26c8dc
--- /dev/null
@@ -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 (file)
index 0000000..d6fd2cd
--- /dev/null
@@ -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 (file)
index 0000000..1119def
--- /dev/null
@@ -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 (file)
index 0000000..4c8a915
--- /dev/null
@@ -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
index a2b4907..d780d9c 100644 (file)
@@ -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
index 65c7e48..066fc3f 100644 (file)
@@ -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
index 41ecc48..8f5f0ee 100644 (file)
@@ -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
index 8b5a4ed..102bc44 100644 (file)
@@ -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"
index 8981705..c1fe00c 100644 (file)
@@ -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
 
index fd9dce7..7f7492b 100644 (file)
@@ -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 (file)
index 0000000..50942b3
--- /dev/null
@@ -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
index 48a73f0..f001c0b 100644 (file)
@@ -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 (file)
index 0000000..2634ea1
--- /dev/null
@@ -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
index 628a179..21b6159 100644 (file)
@@ -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 (file)
index 0000000..543e431
--- /dev/null
@@ -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 (file)
index 0000000..3afe019
--- /dev/null
@@ -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 (file)
index 0000000..1fac8d0
--- /dev/null
@@ -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 (file)
index 0000000..15d6da6
--- /dev/null
@@ -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
index 9571d49..5d97d50 100644 (file)
@@ -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 (file)
index 0000000..55c947e
--- /dev/null
@@ -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
index 025f44f..0d8cac5 100644 (file)
@@ -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
index 53e2e8c..a2983e1 100644 (file)
@@ -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
index a25bbad..aee12e7 100644 (file)
@@ -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 (file)
index 0000000..2850125
--- /dev/null
@@ -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 (file)
index 0000000..bee6544
--- /dev/null
@@ -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
index ddc403c..6dc051e 100644 (file)
@@ -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
index 2ed5ea2..5db17b7 100644 (file)
@@ -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:
index 62a510f..c5e3d04 100644 (file)
@@ -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
index 31b7272..6ee41e8 100644 (file)
@@ -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
index 4f2c86e..318adbf 100644 (file)
@@ -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:
index b250065..be7a378 100644 (file)
@@ -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
index 65d99d1..d071938 100644 (file)
@@ -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
index 59c2449..3d96eab 100644 (file)
@@ -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
index ec5451d..11d4eac 100644 (file)
@@ -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}")
 
index d2eb4a2..c907a98 100755 (executable)
@@ -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
index f9e2538..b596f80 100644 (file)
@@ -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 (file)
index 1a0f6ea..0000000
+++ /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
index ba1af08..229f496 100644 (file)
@@ -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
index 23b18fb..0f206f4 100644 (file)
@@ -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 (file)
index 0000000..e69de29
diff --git a/spec/javascripts/stat_graph_contributors_graph_spec.js b/spec/javascripts/stat_graph_contributors_graph_spec.js
new file mode 100644 (file)
index 0000000..8d2e203
--- /dev/null
@@ -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 (file)
index 0000000..367f0af
--- /dev/null
@@ -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 (file)
index 0000000..b888176
--- /dev/null
@@ -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 (file)
index 0000000..9bfa261
--- /dev/null
@@ -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 (file)
index 0000000..986a4c1
--- /dev/null
@@ -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/models/project_snippet_spec.rb b/spec/models/project_snippet_spec.rb
new file mode 100644 (file)
index 0000000..716fd81
--- /dev/null
@@ -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
index 04b4ce1..2e3870b 100644 (file)
@@ -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) }
index e4d1934..52355c3 100644 (file)
@@ -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) }
index 7559c4c..4dd2048 100644 (file)
@@ -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)
index 78d55a7..11296ae 100644 (file)
@@ -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) }
index de0631d..3107514 100644 (file)
@@ -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)
index 3117631..13e6627 100644 (file)
@@ -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)
index f20a1ca..b2b20ef 100644 (file)
@@ -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
index b6135b4..aa3952f 100644 (file)
@@ -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