diff options
124 files changed, 1746 insertions, 660 deletions
diff --git a/.gitignore b/.gitignore index 8c626989bb8..725f289db55 100644 --- a/.gitignore +++ b/.gitignore @@ -6,6 +6,7 @@ log/*.log tmp/ .sass-cache/ coverage/* +backups/* *.swp public/uploads/ .rvmrc diff --git a/CHANGELOG b/CHANGELOG index 172357603cb..ec5d2f220d3 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,5 +1,13 @@ v 2.7.0 - Issue Labels + - Inline diff + - Git HTTP + - API + - UI improved + - System hooks + - UI improved + - Dashboard events endless scroll + - Source perfomance increased v 2.6.0 - UI polished @@ -7,7 +7,7 @@ gem "sqlite3" gem "mysql2" # Auth -gem "devise", "~> 1.5" +gem "devise", "~> 2.1.0" # GITLAB patched libs gem "grit", :git => "https://github.com/gitlabhq/grit.git", :ref => "7f35cb98ff17d534a07e3ce6ec3d580f67402837" @@ -71,7 +71,6 @@ group :development, :test do gem "awesome_print" gem "database_cleaner" gem "launchy" - gem "webmock" end group :test do @@ -82,4 +81,5 @@ group :test do gem "shoulda-matchers" gem 'email_spec' gem 'resque_spec' + gem "webmock" end diff --git a/Gemfile.lock b/Gemfile.lock index e6a488f2753..e4c06fed229 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -148,10 +148,11 @@ GEM nokogiri (>= 1.5.0) daemons (1.1.8) database_cleaner (0.8.0) - devise (1.5.3) + devise (2.1.2) bcrypt-ruby (~> 3.0) - orm_adapter (~> 0.0.3) - warden (~> 1.1) + orm_adapter (~> 0.1) + railties (~> 3.1) + warden (~> 1.2.1) diff-lcs (1.1.3) drapper (0.8.4) email_spec (1.2.1) @@ -225,7 +226,7 @@ GEM omniauth (1.1.0) hashie (~> 1.2) rack - orm_adapter (0.0.7) + orm_adapter (0.3.0) polyglot (0.3.3) posix-spawn (0.3.6) pry (0.9.9.6) @@ -356,7 +357,7 @@ GEM raindrops (~> 0.7) vegas (0.1.11) rack (>= 1.0.0) - warden (1.2.0) + warden (1.2.1) rack (>= 1.0) webmock (1.8.7) addressable (>= 2.2.7) @@ -383,7 +384,7 @@ DEPENDENCIES colored cucumber-rails database_cleaner - devise (~> 1.5) + devise (~> 2.1.0) drapper email_spec ffaker @@ -1 +1 @@ -2.7.0pre +2.7.0 diff --git a/app/assets/images/ajax_loader_tree.gif b/app/assets/images/ajax_loader_tree.gif Binary files differnew file mode 100644 index 00000000000..99d5a0f37f3 --- /dev/null +++ b/app/assets/images/ajax_loader_tree.gif diff --git a/app/assets/javascripts/application.js b/app/assets/javascripts/application.js index 42290f8e154..527b5c795e1 100644 --- a/app/assets/javascripts/application.js +++ b/app/assets/javascripts/application.js @@ -12,6 +12,7 @@ //= require jquery.cookie //= require jquery.endless-scroll //= require jquery.highlight +//= require jquery.waitforimages //= require bootstrap-modal //= require modernizr //= require chosen-jquery @@ -20,10 +21,26 @@ //= require_tree . $(document).ready(function(){ + $(".one_click_select").live("click", function(){ $(this).select(); }); + + $('body').on('ajax:complete, ajax:beforeSend, submit', 'form', function(e){ + var buttons = $('[type="submit"]', this); + switch( e.type ){ + case 'ajax:beforeSend': + case 'submit': + buttons.attr('disabled', 'disabled'); + break; + case ' ajax:complete': + default: + buttons.removeAttr('disabled'); + break; + } + }) + $(".account-box").mouseenter(showMenu); $(".account-box").mouseleave(resetMenu); @@ -97,3 +114,8 @@ function showDiff(link) { return _chosen.apply(this, [default_options]); }}) })(jQuery); + + +function ajaxGet(url) { + $.ajax({type: "GET", url: url, dataType: "script"}); +} diff --git a/app/assets/javascripts/issues.js b/app/assets/javascripts/issues.js index 49936e3f0ee..0acf9ec8aef 100644 --- a/app/assets/javascripts/issues.js +++ b/app/assets/javascripts/issues.js @@ -73,4 +73,25 @@ function issuesPage(){ $("#milestone_id, #assignee_id, #label_name").on("change", function(){ $(this).closest("form").submit(); }); + + $('body').on('ajax:success', '.close_issue, .reopen_issue, #new_issue', function(){ + var t = $(this), + totalIssues, + reopen = t.hasClass('reopen_issue'), + newIssue = false; + if( this.id == 'new_issue' ){ + newIssue = true; + } + $('.issue_counter, #new_issue').each(function(){ + var issue = $(this); + totalIssues = parseInt( $(this).html(), 10 ); + + if( newIssue || ( reopen && issue.closest('.main_menu').length ) ){ + $(this).html( totalIssues+1 ); + }else { + $(this).html( totalIssues-1 ); + } + }); + + }); } diff --git a/app/assets/javascripts/note.js b/app/assets/javascripts/note.js index 4d97ffefdce..c45a45d2fcb 100644 --- a/app/assets/javascripts/note.js +++ b/app/assets/javascripts/note.js @@ -25,11 +25,11 @@ init: $(this).closest('li').fadeOut(); }); $("#new_note").live("ajax:before", function(){ - $("#submit_note").attr("disabled", "disabled"); + $(".submit_note").attr("disabled", "disabled"); }) $("#new_note").live("ajax:complete", function(){ - $("#submit_note").removeAttr("disabled"); + $(".submit_note").removeAttr("disabled"); }) $("#note_note").live("focus", function(){ diff --git a/app/assets/stylesheets/common.scss b/app/assets/stylesheets/common.scss index 19092073c17..0db803aae23 100644 --- a/app/assets/stylesheets/common.scss +++ b/app/assets/stylesheets/common.scss @@ -604,7 +604,11 @@ li.note { border-style: solid; border-width: 1px; @include border-radius(4px); - min-height:42px; + min-height:22px; + + .avatar { + width:24px; + } } .supp_diff_link, diff --git a/app/assets/stylesheets/gitlab_bootstrap.scss b/app/assets/stylesheets/gitlab_bootstrap.scss index 1b86cddee39..39e5998305a 100644 --- a/app/assets/stylesheets/gitlab_bootstrap.scss +++ b/app/assets/stylesheets/gitlab_bootstrap.scss @@ -202,6 +202,10 @@ a:focus { color:$style_color; } +.nav-tabs > .active > a { + font-weight:bold; +} + /** COLORS **/ .cgray { color:gray; } .cred { color:#D12F19; } @@ -209,6 +213,7 @@ a:focus { .cblack { color:#111; } .cdark { color:#444 } .cwhite { color:#fff !important } +.bgred { background: #F2DEDE !important} /** COMMON STYLES **/ .left { @@ -299,9 +304,24 @@ table.no-borders { } .event_label { - background: #FCEEC1; - padding: 2px 2px 0; - font-family: monospace; + @extend .label; + background-color: #999; + + &.pushed { + background-color: #3A87AD; + } + + &.opened { + background-color: #468847; + } + + &.closed { + background-color: #B94A48; + } + + &.merged { + background-color: #2A2; + } } img.avatar { @@ -425,9 +445,10 @@ form { */ .ui-box { background:#F9F9F9; - margin-bottom: 40px; + margin-bottom: 25px; @include round-borders-all(4px); border-color: #CCC; + @include solid_shade; ul { margin:0; @@ -443,6 +464,13 @@ form { background-image: -moz-linear-gradient(#eee 6.6%, #dfdfdf); background-image: -o-linear-gradient(#eee 6.6%, #dfdfdf); + &.small { + line-height: 28px; + font-size: 14px; + line-height:28px; + text-shadow: 0 1px 1px white; + } + form { padding:9px 0; margin:0px; @@ -511,6 +539,7 @@ form { table.admin-table { @extend .table-bordered; @extend .zebra-striped; + @include solid_shade; th { border-color: #CCC; border-bottom: 1px solid #bbb; @@ -568,6 +597,8 @@ ul.breadcrumb { @extend .prepend-top-20; @extend .append-bottom-20; border-width:1px; + @include solid_shade; + img { max-width: 100%; } @@ -624,13 +655,166 @@ p { h3.page_title { color:#456; font-size:20px; - font-weight: 600; + font-weight: normal; line-height: 28px; } -pre.logs { - .log { - font-size:12px; - line-height:18px; +/** + * File content holder + * + */ +.file_holder { + border:1px solid #CCC; + margin-bottom:1em; + @include solid_shade; + + .file_title { + border-bottom: 1px solid #bbb; + background:#eee; + background-image: -webkit-gradient(linear, 0 0, 0 30, color-stop(0.066, #eee), to(#dfdfdf)); + background-image: -webkit-linear-gradient(#eee 6.6%, #dfdfdf); + background-image: -moz-linear-gradient(#eee 6.6%, #dfdfdf); + background-image: -o-linear-gradient(#eee 6.6%, #dfdfdf); + margin: 0; + font-weight: normal; + font-weight: bold; + text-align: left; + color: #666; + padding: 9px 10px; + height:18px; + + .options { + float:right; + margin-top: -5px; + } + + .file_name { + color:$style_color; + font-size:14px; + text-shadow: 0 1px 1px #fff; + small { + color:#999; + font-size:13px; + } + } + } + .file_content { + background:#fff; + font-size: 11px; + + &.wiki { + font-size: 13px; + code { + padding:0 4px; + } + padding:20px; + h1, h2 { + line-height: 46px; + } + h3, h4 { + line-height: 40px; + } + } + + &.image_file { + background:#eee; + text-align:center; + img { + padding:100px; + max-width:300px; + } + } + + &.blob_file { + + } + + /** + * Blame file + */ + &.blame { + tr { + border-bottom: 1px solid #eee; + } + td { + padding:5px; + } + .author, + .blame_commit { + background:#f5f5f5; + vertical-align:top; + } + .lines { + pre { + padding:0; + margin:0; + background:none; + border:none; + } + } + } + + &.logs { + background:#eee; + max-height: 700px; + overflow-y: auto; + + ol { + margin-left:40px; + padding: 10px 0; + border-left: 1px solid #CCC; + margin-bottom:0; + background: white; + li { + color:#888; + p { + margin:0; + color:#333; + line-height:24px; + padding-left: 10px; + } + + &:hover { + background:$hover; + } + } + } + } + + /** + * Code file + */ + &.code { + padding:0; + td.code { + width: 100%; + .highlight { + margin-left: 55px; + overflow:auto; + overflow-y:hidden; + } + } + .highlight pre { + white-space: pre; + word-wrap:normal; + } + + table.highlighttable { + border: none; + } + body.project-page table.highlighttable td { border: none } + table.highlighttable tr:hover { background:none;} + + table.highlighttable pre{ + line-height:16px !important; + font-size:12px !important; + } + + table.highlighttable .linenodiv pre { + text-align: right; + padding-right: 4px; + color:#666; + } + } } } diff --git a/app/assets/stylesheets/header.scss b/app/assets/stylesheets/header.scss index 5e2e410071b..07eba39b275 100644 --- a/app/assets/stylesheets/header.scss +++ b/app/assets/stylesheets/header.scss @@ -96,7 +96,7 @@ header { */ .search { float: right; - margin-right: 55px; + margin-right: 50px; .search-input { @extend .span2; @@ -126,10 +126,10 @@ header { cursor: pointer; img { border-radius: 4px; - right: 0px; + right: 5px; position: absolute; - width: 33px; - height: 33px; + width: 31px; + height: 31px; display: block; top: 0; &:after { diff --git a/app/assets/stylesheets/main.scss b/app/assets/stylesheets/main.scss index bff24dd68b9..7d60cd258e3 100644 --- a/app/assets/stylesheets/main.scss +++ b/app/assets/stylesheets/main.scss @@ -31,6 +31,12 @@ $hover: #FDF5D9; box-shadow: 0 0 3px #ddd; } +@mixin solid_shade { + -moz-box-shadow: 0 0 0 3px #eee; + -webkit-box-shadow: 0 0 0 3px #eee; + box-shadow: 0 0 0 3px #eee; +} + @mixin border-radius($radius) { -moz-border-radius: $radius; -webkit-border-radius: $radius; @@ -136,7 +142,7 @@ $hover: #FDF5D9; /** * Code (files list) styles. Browsing project files there */ -@import "tree.scss"; +@import "sections/tree.scss"; /** * This file represent notes(comments) styles diff --git a/app/assets/stylesheets/notes.scss b/app/assets/stylesheets/notes.scss index 70a31dbb0ff..39db704b1a9 100644 --- a/app/assets/stylesheets/notes.scss +++ b/app/assets/stylesheets/notes.scss @@ -63,18 +63,22 @@ p.notify_controls span{ tr.line_notes_row { border-bottom:1px solid #DDD; + border-left: 7px solid #2A79A3; + &.reply { background:#eee; - + border-left: 7px solid #2A79A3; + border-top:1px solid #ddd; td { padding:7px 10px; } a.line_note_reply_link { @include round-borders-all(4px); - border-color:#aaa; - background: #bbb; - padding: 3px 20px; + padding: 3px 10px; + margin-left:5px; color: white; + background: #2A79A3; + border-color: #2A79A3; } } ul { @@ -95,6 +99,9 @@ tr.line_notes_row { td { border-bottom:1px solid #ddd; } + .actions { + margin:0; + } } td .line_note_link { diff --git a/app/assets/stylesheets/sections/commits.scss b/app/assets/stylesheets/sections/commits.scss index acab785ac71..6052ec3fabb 100644 --- a/app/assets/stylesheets/sections/commits.scss +++ b/app/assets/stylesheets/sections/commits.scss @@ -101,18 +101,21 @@ margin:50px; padding:1px; max-width:400px; - } - &.diff_image_removed { - img { + + &.diff_image_removed { border: 1px solid #C00; } - } - &.diff_image_added { - img { + &.diff_image_added { border: 1px solid #0C0;; } } + + &.img_compared { + img { + max-width:300px; + } + } } } diff --git a/app/assets/stylesheets/sections/merge_requests.scss b/app/assets/stylesheets/sections/merge_requests.scss index ad496238a13..34f43acf839 100644 --- a/app/assets/stylesheets/sections/merge_requests.scss +++ b/app/assets/stylesheets/sections/merge_requests.scss @@ -82,3 +82,15 @@ } } } + +li.merge_request { + padding:7px 10px; + img.avatar { + width: 32px; + margin-top: 4px; + } + p { + padding: 0px; + padding-bottom: 2px; + } +} diff --git a/app/assets/stylesheets/sections/tree.scss b/app/assets/stylesheets/sections/tree.scss new file mode 100644 index 00000000000..c915e0a96fb --- /dev/null +++ b/app/assets/stylesheets/sections/tree.scss @@ -0,0 +1,96 @@ +#tree-holder { + #tree-content-holder { + float:left; + width:100%; + } + #tree-readme-holder { + float:left; + width:100%; + .readme { + border:1px solid #ccc; + padding:12px; + background: #F7F7F7; + + pre { + overflow: auto; + } + } + } + + .tree_progress { + display:none; + margin:20px; + &.loading { + display:block; + } + } + + #tree-slider { + @include border-radius(0); + .tree-item { + &:hover { + td { background: $hover; } + cursor:pointer; + } + } + } + + .tree-item { + .tree-item-file-name { + vertical-align:middle; + font-weight:bold; + a { + color:$style_color; + &:hover { + color:$blue_link; + } + } + + img { + position: relative; + top:-1px; + } + } + } + + + #tree-slider { + @include solid_shade; + width:100%; + + border-color:#ccc; + + td { + padding:8px; + border-color:#f1f1f1; + background:#fafafa; + } + + tr:first-child td:first-child, + tr:first-child td:last-child { + border-radius:0; + } + + th { + border-color: #CCC; + border-bottom: 1px solid #bbb; + background:#eee; + background-image: -webkit-gradient(linear, 0 0, 0 30, color-stop(0.066, #eee), to(#dfdfdf)); + background-image: -webkit-linear-gradient(#eee 6.6%, #dfdfdf); + background-image: -moz-linear-gradient(#eee 6.6%, #dfdfdf); + background-image: -o-linear-gradient(#eee 6.6%, #dfdfdf); + } + } + + .tree-commit-link { + color:#333; + } + + a.tree-commit-link { + color: #666; + &:hover { + text-decoration: underline; + } + } + +} diff --git a/app/assets/stylesheets/themes/ui_mars.scss b/app/assets/stylesheets/themes/ui_mars.scss index 0fea6144431..39dcab1d085 100644 --- a/app/assets/stylesheets/themes/ui_mars.scss +++ b/app/assets/stylesheets/themes/ui_mars.scss @@ -70,8 +70,7 @@ } } .separator { - border-color:#444; - background:#31363E; + display:none; } } diff --git a/app/assets/stylesheets/tree.scss b/app/assets/stylesheets/tree.scss deleted file mode 100644 index 912c63ee16b..00000000000 --- a/app/assets/stylesheets/tree.scss +++ /dev/null @@ -1,232 +0,0 @@ -#tree-holder { - #tree-content-holder { - float:left; - width:100%; - } - #tree-readme-holder { - float:left; - width:100%; - .readme { - border:1px solid #ccc; - padding:12px; - background: #F7F7F7; - - pre { - overflow: auto; - } - } - } - - .tree_progress { - display:none; - margin:20px; - &.loading { - display:block; - } - } - - - /** FILE CONTENT VIEW **/ - .view_file_content{ - .old_line, .new_line { - background:#ECECEC; - color:#777; - width:15px; - float:left; - padding: 0px 10px; - border-right: 1px solid #ccc; - } - .old_line{ - display:none; - } - } - - .view_file .view_file_header, - .diff_file .diff_file_header { - border-bottom: 1px solid #bbb; - background:#eee; - background-image: -webkit-gradient(linear, 0 0, 0 30, color-stop(0.066, #eee), to(#dfdfdf)); - background-image: -webkit-linear-gradient(#eee 6.6%, #dfdfdf); - background-image: -moz-linear-gradient(#eee 6.6%, #dfdfdf); - background-image: -o-linear-gradient(#eee 6.6%, #dfdfdf); - margin: 0; - font-weight: normal; - font-weight: bold; - text-align: left; - color: #666; - padding: 9px 10px; - height:18px; - - .options { - float:right; - margin-top: -5px; - } - - .file_name { - color:$style_color; - font-size:14px; - text-shadow: 0 1px 1px #fff; - small { - color:#999; - font-size:13px; - } - } - } - - .view_file { - border:1px solid #CCC; - margin-bottom:1em; - - .view_file_content { - background:#fff; - color:#514721; - font-size: 11px; - } - .view_file_content_image { - background:#eee; - text-align:center; - img { - padding:100px; - max-width:300px; - } - } - } - - td.code { - width: 100%; - .highlight { - margin-left: 55px; - overflow:auto; - overflow-y:hidden; - } - } - .highlight pre { - white-space: pre; - word-wrap:normal; - } - - table.highlighttable { - border: none; - } - body.project-page table.highlighttable td { border: none } - table.highlighttable tr:hover { background:none;} - - table.highlighttable pre{ - line-height:16px !important; - font-size:12px !important; - } - - table.highlighttable .linenodiv pre { - text-align: right; - padding-right: 4px; - color:#666; - } - - #tree-slider { - @include border-radius(0); - .tree-item { - &:hover { - td { background: $hover; } - cursor:pointer; - } - } - } - - .tree-item { - .tree-item-file-name { - vertical-align:middle; - font-weight:bold; - a { - color:$style_color; - &:hover { - color:$blue_link; - } - } - - img { - position: relative; - top:-1px; - } - } - } - - - #tree-slider { - @include shade; - width:100%; - - border-color:#ccc; - - td { - padding:8px; - border-color:#f1f1f1; - background:#fafafa; - } - - tr:first-child td:first-child, - tr:first-child td:last-child { - border-radius:0; - } - - th { - border-color: #CCC; - border-bottom: 1px solid #bbb; - background:#eee; - background-image: -webkit-gradient(linear, 0 0, 0 30, color-stop(0.066, #eee), to(#dfdfdf)); - background-image: -webkit-linear-gradient(#eee 6.6%, #dfdfdf); - background-image: -moz-linear-gradient(#eee 6.6%, #dfdfdf); - background-image: -o-linear-gradient(#eee 6.6%, #dfdfdf); - } - } - - .tree-commit-link { - color:#333; - } - - #tree-content-holder .view_file{ - @include shade; - } - - #tree-readme-holder .readme { - @include shade; - margin-bottom:20px; - h1, h2 { - line-height: 56px; - } - h3, h4 { - line-height: 46px; - } - } - - a.tree-commit-link { - color: #666; - &:hover { - text-decoration: underline; - } - } - -} - -.blame_file { - .view_file_content { - tr { - border-bottom: 1px solid #eee; - } - td { - padding:5px; - } - .author, - .commit { - background:#f5f5f5; - vertical-align:top; - } - .lines { - pre { - padding:0; - margin:0; - background:none; - border:none; - } - } - } -} diff --git a/app/contexts/base_context.rb b/app/contexts/base_context.rb new file mode 100644 index 00000000000..6eb8ee46c80 --- /dev/null +++ b/app/contexts/base_context.rb @@ -0,0 +1,8 @@ +class BaseContext + attr_accessor :project, :current_user, :params + + def initialize(project, user, params) + @project, @current_user, @params = project, user, params.dup + end +end + diff --git a/app/contexts/commit_load.rb b/app/contexts/commit_load.rb new file mode 100644 index 00000000000..bab30d61007 --- /dev/null +++ b/app/contexts/commit_load.rb @@ -0,0 +1,26 @@ +class CommitLoad < BaseContext + def execute + result = { + :commit => nil, + :suppress_diff => false, + :line_notes => [], + :notes_count => 0, + :note => nil + } + + commit = project.commit(params[:id]) + + if commit + commit = CommitDecorator.decorate(commit) + line_notes = project.commit_line_notes(commit) + + result[:suppress_diff] = true if commit.diffs.size > 200 && !params[:force_show_diff] + result[:commit] = commit + result[:note] = project.build_commit_note(commit) + result[:line_notes] = line_notes + result[:notes_count] = line_notes.count + project.commit_notes(commit).count + end + + result + end +end diff --git a/app/contexts/merge_requests_load.rb b/app/contexts/merge_requests_load.rb new file mode 100644 index 00000000000..6778db3bce5 --- /dev/null +++ b/app/contexts/merge_requests_load.rb @@ -0,0 +1,16 @@ +class MergeRequestsLoad < BaseContext + def execute + type = params[:f].to_i + + merge_requests = project.merge_requests + + merge_requests = case type + when 1 then merge_requests + when 2 then merge_requests.closed + when 3 then merge_requests.opened.assigned(current_user) + else merge_requests.opened + end.page(params[:page]).per(20) + + merge_requests.includes(:author, :project).order("closed, created_at desc") + end +end diff --git a/app/contexts/notes_load.rb b/app/contexts/notes_load.rb new file mode 100644 index 00000000000..d1f8da9ce12 --- /dev/null +++ b/app/contexts/notes_load.rb @@ -0,0 +1,30 @@ +class NotesLoad < BaseContext + def execute + target_type = params[:target_type] + target_id = params[:target_id] + first_id = params[:first_id] + last_id = params[:last_id] + + + @notes = case target_type + when "commit" + then project.commit_notes(project.commit(target_id)).fresh.limit(20) + when "snippet" + then project.snippets.find(target_id).notes + when "wall" + then project.common_notes.order("created_at DESC").fresh.limit(50) + when "issue" + then project.issues.find(target_id).notes.inc_author.order("created_at DESC").limit(20) + when "merge_request" + then project.merge_requests.find(target_id).notes.inc_author.order("created_at DESC").limit(20) + end + + @notes = if last_id + @notes.where("id > ?", last_id) + elsif first_id + @notes.where("id < ?", first_id) + else + @notes + end + end +end diff --git a/app/controllers/admin/hooks_controller.rb b/app/controllers/admin/hooks_controller.rb new file mode 100644 index 00000000000..7f832fd5697 --- /dev/null +++ b/app/controllers/admin/hooks_controller.rb @@ -0,0 +1,44 @@ +class Admin::HooksController < ApplicationController + layout "admin" + before_filter :authenticate_user! + before_filter :authenticate_admin! + + def index + @hooks = SystemHook.all + @hook = SystemHook.new + end + + def create + @hook = SystemHook.new(params[:hook]) + + if @hook.save + redirect_to admin_hooks_path, notice: 'Hook was successfully created.' + else + @hooks = SystemHook.all + render :index + end + end + + def destroy + @hook = SystemHook.find(params[:id]) + @hook.destroy + + redirect_to admin_hooks_path + end + + + def test + @hook = SystemHook.find(params[:hook_id]) + data = { + event_name: "project_create", + name: "Ruby", + path: "ruby", + project_id: 1, + owner_name: "Someone", + owner_email: "example@gitlabhq.com" + } + @hook.execute(data) + + redirect_to :back + end +end diff --git a/app/controllers/admin/mailer_controller.rb b/app/controllers/admin/mailer_controller.rb deleted file mode 100644 index 2352e189204..00000000000 --- a/app/controllers/admin/mailer_controller.rb +++ /dev/null @@ -1,45 +0,0 @@ -class Admin::MailerController < ApplicationController - layout "admin" - before_filter :authenticate_user! - before_filter :authenticate_admin! - - def preview - - end - - def preview_note - @note = Note.first - @user = @note.author - @project = @note.project - case params[:type] - when "Commit" then - @commit = @project.commit - render :file => 'notify/note_commit_email', :layout => 'notify' - when "Issue" then - @issue = Issue.first - render :file => 'notify/note_issue_email', :layout => 'notify' - else - render :file => 'notify/note_wall_email', :layout => 'notify' - end - rescue - render :text => "Preview not available" - end - - def preview_user_new - @user = User.first - @password = "DHasJKDHAS!" - - render :file => 'notify/new_user_email', :layout => 'notify' - rescue - render :text => "Preview not available" - end - - def preview_issue_new - @issue = Issue.first - @user = @issue.assignee - @project = @issue.project - render :file => 'notify/new_issue_email', :layout => 'notify' - rescue - render :text => "Preview not available" - end -end diff --git a/app/controllers/admin/projects_controller.rb b/app/controllers/admin/projects_controller.rb index 5266b406504..0ff97bf2c32 100644 --- a/app/controllers/admin/projects_controller.rb +++ b/app/controllers/admin/projects_controller.rb @@ -6,7 +6,7 @@ class Admin::ProjectsController < ApplicationController def index @admin_projects = Project.scoped @admin_projects = @admin_projects.search(params[:name]) if params[:name].present? - @admin_projects = @admin_projects.page(params[:page]) + @admin_projects = @admin_projects.page(params[:page]).per(20) end def show @@ -72,6 +72,6 @@ class Admin::ProjectsController < ApplicationController @admin_project = Project.find_by_code(params[:id]) @admin_project.destroy - redirect_to admin_projects_url + redirect_to admin_projects_url, notice: 'Project was successfully deleted.' end end diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 9a0f95bf0cb..3265046d2ae 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -52,7 +52,7 @@ class ApplicationController < ActionController::Base def layout_by_resource if devise_controller? - "devise" + "devise_layout" else "application" end diff --git a/app/controllers/commits_controller.rb b/app/controllers/commits_controller.rb index 25e980e017b..cb1f74527a1 100644 --- a/app/controllers/commits_controller.rb +++ b/app/controllers/commits_controller.rb @@ -26,43 +26,31 @@ class CommitsController < ApplicationController end def show - @commit = project.commit(params[:id]) - - git_not_found! and return unless @commit - - @commit = CommitDecorator.decorate(@commit) - - @note = @project.build_commit_note(@commit) - @comments_allowed = true - @line_notes = project.commit_line_notes(@commit) - - @notes_count = @line_notes.count + project.commit_notes(@commit).count - - if @commit.diffs.size > 200 && !params[:force_show_diff] - @suppress_diff = true + result = CommitLoad.new(project, current_user, params).execute + + @commit = result[:commit] + + if @commit + @suppress_diff = result[:suppress_diff] + @note = result[:note] + @line_notes = result[:line_notes] + @notes_count = result[:notes_count] + @comments_allowed = true + else + return git_not_found! end + rescue Grit::Git::GitTimeout render "huge_commit" end def compare - first = project.commit(params[:to].try(:strip)) - last = project.commit(params[:from].try(:strip)) + result = Commit.compare(project, params[:from], params[:to]) - @diffs = [] - @commits = [] + @commits = result[:commits] + @commit = result[:commit] + @diffs = result[:diffs] @line_notes = [] - - if first && last - commits = [first, last].sort_by(&:created_at) - younger = commits.first - older = commits.last - - - @commits = project.repo.commits_between(younger.id, older.id).map {|c| Commit.new(c)} - @diffs = project.repo.diff(younger.id, older.id) rescue [] - @commit = Commit.new(older) - end end def patch diff --git a/app/controllers/dashboard_controller.rb b/app/controllers/dashboard_controller.rb index a054940738e..8508e2454d2 100644 --- a/app/controllers/dashboard_controller.rb +++ b/app/controllers/dashboard_controller.rb @@ -2,15 +2,13 @@ class DashboardController < ApplicationController respond_to :html def index - @projects = current_user.projects.includes(:events).order("events.created_at DESC") - @projects = @projects.page(params[:page]).per(40) - - @events = Event.where(:project_id => current_user.projects.map(&:id)).recent.limit(20) - + @projects = current_user.projects_with_events.page(params[:page]).per(40) + @events = Event.recent_for_user(current_user).limit(20).offset(params[:offset] || 0) @last_push = current_user.recent_push respond_to do |format| format.html + format.js format.atom { render :layout => false } end end diff --git a/app/controllers/hooks_controller.rb b/app/controllers/hooks_controller.rb index 9627aba9771..ad2fb3ae781 100644 --- a/app/controllers/hooks_controller.rb +++ b/app/controllers/hooks_controller.rb @@ -11,24 +11,24 @@ class HooksController < ApplicationController respond_to :html def index - @hooks = @project.web_hooks.all - @hook = WebHook.new + @hooks = @project.hooks.all + @hook = ProjectHook.new end def create - @hook = @project.web_hooks.new(params[:hook]) + @hook = @project.hooks.new(params[:hook]) @hook.save if @hook.valid? redirect_to project_hooks_path(@project) else - @hooks = @project.web_hooks.all + @hooks = @project.hooks.all render :index end end def test - @hook = @project.web_hooks.find(params[:id]) + @hook = @project.hooks.find(params[:id]) commits = @project.commits(@project.default_branch, nil, 3) data = @project.post_receive_data(commits.last.id, commits.first.id, "refs/heads/#{@project.default_branch}", current_user) @hook.execute(data) @@ -37,7 +37,7 @@ class HooksController < ApplicationController end def destroy - @hook = @project.web_hooks.find(params[:id]) + @hook = @project.hooks.find(params[:id]) @hook.destroy redirect_to project_hooks_path(@project) diff --git a/app/controllers/merge_requests_controller.rb b/app/controllers/merge_requests_controller.rb index ec4ed45fedf..1cb1d388465 100644 --- a/app/controllers/merge_requests_controller.rb +++ b/app/controllers/merge_requests_controller.rb @@ -24,16 +24,7 @@ class MergeRequestsController < ApplicationController def index - @merge_requests = @project.merge_requests - - @merge_requests = case params[:f].to_i - when 1 then @merge_requests - when 2 then @merge_requests.closed - when 3 then @merge_requests.opened.assigned(current_user) - else @merge_requests.opened - end.page(params[:page]).per(20) - - @merge_requests = @merge_requests.includes(:author, :project).order("closed, created_at desc") + @merge_requests = MergeRequestsLoad.new(project, current_user, params).execute end def show diff --git a/app/controllers/notes_controller.rb b/app/controllers/notes_controller.rb index a2638d9597c..1c997e380a0 100644 --- a/app/controllers/notes_controller.rb +++ b/app/controllers/notes_controller.rb @@ -40,25 +40,6 @@ class NotesController < ApplicationController protected def notes - @notes = case params[:target_type] - when "commit" - then project.commit_notes(project.commit((params[:target_id]))).fresh.limit(20) - when "snippet" - then project.snippets.find(params[:target_id]).notes - when "wall" - then project.common_notes.order("created_at DESC").fresh.limit(50) - when "issue" - then project.issues.find(params[:target_id]).notes.inc_author.order("created_at DESC").limit(20) - when "merge_request" - then project.merge_requests.find(params[:target_id]).notes.inc_author.order("created_at DESC").limit(20) - end - - @notes = if params[:last_id] - @notes.where("id > ?", params[:last_id]) - elsif params[:first_id] - @notes.where("id < ?", params[:first_id]) - else - @notes - end + @notes = NotesLoad.new(project, current_user, params).execute end end diff --git a/app/controllers/omniauth_callbacks_controller.rb b/app/controllers/omniauth_callbacks_controller.rb index 629b6819fb1..fb759c371c4 100644 --- a/app/controllers/omniauth_callbacks_controller.rb +++ b/app/controllers/omniauth_callbacks_controller.rb @@ -1,4 +1,17 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController + + # Extend the standard message generation to accept our custom exception + def failure_message + exception = env["omniauth.error"] + if exception.class == OmniAuth::Error + error = exception.message + else + error = exception.error_reason if exception.respond_to?(:error_reason) + error ||= exception.error if exception.respond_to?(:error) + error ||= env["omniauth.error.type"].to_s + end + error.to_s.humanize if error + end def ldap # We only find ourselves here if the authentication to LDAP was successful. diff --git a/app/controllers/refs_controller.rb b/app/controllers/refs_controller.rb index ddf5f675d0c..b610c9f34d4 100644 --- a/app/controllers/refs_controller.rb +++ b/app/controllers/refs_controller.rb @@ -9,7 +9,7 @@ class RefsController < ApplicationController before_filter :require_non_empty_project before_filter :ref - before_filter :define_tree_vars, :only => [:tree, :blob, :blame] + before_filter :define_tree_vars, :only => [:tree, :blob, :blame, :logs_tree] before_filter :render_full_content layout "project" @@ -46,6 +46,18 @@ class RefsController < ApplicationController end end + def logs_tree + contents = @tree.contents + @logs = contents.map do |content| + file = params[:path] ? File.join(params[:path], content.name) : content.name + last_commit = @project.commits(@commit.id, file, 1).last + { + :file_name => content.name, + :commit => last_commit + } + end + end + def blob if @tree.is_blob? if @tree.text? @@ -79,6 +91,15 @@ class RefsController < ApplicationController @commit = project.commit(@ref) @tree = Tree.new(@commit.tree, project, @ref, params[:path]) @tree = TreeDecorator.new(@tree) + @hex_path = Digest::SHA1.hexdigest(params[:path] || "/") + + if params[:path] + @history_path = tree_file_project_ref_path(@project, @ref, params[:path]) + @logs_path = logs_file_project_ref_path(@project, @ref, params[:path]) + else + @history_path = tree_project_ref_path(@project, @ref) + @logs_path = logs_tree_project_ref_path(@project, @ref) + end rescue return render_404 end diff --git a/app/decorators/event_decorator.rb b/app/decorators/event_decorator.rb new file mode 100644 index 00000000000..50aaa615d49 --- /dev/null +++ b/app/decorators/event_decorator.rb @@ -0,0 +1,25 @@ +class EventDecorator < ApplicationDecorator + decorates :event + + def feed_title + if self.issue? + "#{self.author_name} #{self.action_name} issue ##{self.target_id}:" + self.issue_title + elsif self.merge_request? + "#{self.author_name} #{self.action_name} MR ##{self.target_id}:" + self.merge_request_title + elsif self.push? + "#{self.author_name} #{self.push_action_name} #{self.ref_type} " + self.ref_name + else + "" + end + end + + def feed_url + if self.issue? + h.project_issue_url(self.project, self.issue) + elsif self.merge_request? + h.project_merge_request_url(self.project, self.merge_request) + elsif self.push? + h.project_commits_url(self.project, :ref => self.ref_name) + end + end +end diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index 2697fff433e..3f15fd9237f 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -134,4 +134,8 @@ module ApplicationHelper end active ? "current" : nil end + + def hexdigest(string) + Digest::SHA1.hexdigest string + end end diff --git a/app/helpers/tree_helper.rb b/app/helpers/tree_helper.rb new file mode 100644 index 00000000000..ed3053d8af5 --- /dev/null +++ b/app/helpers/tree_helper.rb @@ -0,0 +1,27 @@ +module TreeHelper + def tree_icon(content) + if content.is_a?(Grit::Blob) + if content.text? + image_tag "file_txt.png" + elsif content.image? + image_tag "file_img.png" + else + image_tag "file_bin.png" + end + else + image_tag "file_dir.png" + end + end + + def tree_hex_class(content) + "file_#{hexdigest(content.name)}" + end + + def tree_full_path(content) + if params[:path] + File.join(params[:path], content.name) + else + content.name + end + end +end diff --git a/app/models/commit.rb b/app/models/commit.rb index 800ad19b9f1..859bee29fa5 100644 --- a/app/models/commit.rb +++ b/app/models/commit.rb @@ -80,6 +80,29 @@ class Commit def commits_between(repo, from, to) repo.commits_between(from, to).map { |c| Commit.new(c) } end + + def compare(project, from, to) + first = project.commit(to.try(:strip)) + last = project.commit(from.try(:strip)) + + result = { + :commits => [], + :diffs => [], + :commit => nil + } + + if first && last + commits = [first, last].sort_by(&:created_at) + younger = commits.first + older = commits.last + + result[:commits] = project.repo.commits_between(younger.id, older.id).map {|c| Commit.new(c)} + result[:diffs] = project.repo.diff(younger.id, older.id) rescue [] + result[:commit] = Commit.new(older) + end + + result + end end def persisted? diff --git a/app/models/event.rb b/app/models/event.rb index dc7dfa16dd5..c75924e7500 100644 --- a/app/models/event.rb +++ b/app/models/event.rb @@ -28,6 +28,10 @@ class Event < ActiveRecord::Base end end + def self.recent_for_user user + where(:project_id => user.projects.map(&:id)).recent + end + # Next events currently enabled for system # - push # - new issue diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb index d3e531f7818..2581f3be4c9 100644 --- a/app/models/merge_request.rb +++ b/app/models/merge_request.rb @@ -22,7 +22,6 @@ class MergeRequest < ActiveRecord::Base :should_remove_source_branch validates_presence_of :project_id - validates_presence_of :assignee_id validates_presence_of :author_id validates_presence_of :source_branch validates_presence_of :target_branch @@ -36,6 +35,7 @@ class MergeRequest < ActiveRecord::Base delegate :name, :email, :to => :assignee, + :allow_nil => true, :prefix => true validates :title, @@ -128,7 +128,7 @@ class MergeRequest < ActiveRecord::Base def unmerged_diffs commits = project.repo.commits_between(target_branch, source_branch).map {|c| Commit.new(c)} - diffs = project.repo.diff(commits.first.prev_commit.id, commits.last.id) + diffs = project.repo.diff(commits.first.prev_commit.id, commits.last.id) rescue [] end def last_commit diff --git a/app/models/project.rb b/app/models/project.rb index ec4893e2b17..a49b3f519b3 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -19,7 +19,7 @@ class Project < ActiveRecord::Base has_many :notes, :dependent => :destroy has_many :snippets, :dependent => :destroy has_many :deploy_keys, :dependent => :destroy, :foreign_key => "project_id", :class_name => "Key" - has_many :web_hooks, :dependent => :destroy + has_many :hooks, :dependent => :destroy, :class_name => "ProjectHook" has_many :wikis, :dependent => :destroy has_many :protected_branches, :dependent => :destroy @@ -120,7 +120,7 @@ class Project < ActiveRecord::Base errors.add(:path, " like 'gitolite-admin' is not allowed") end end - + def self.access_options UsersProject.access_roles end diff --git a/app/models/project_hook.rb b/app/models/project_hook.rb new file mode 100644 index 00000000000..06388aaeb4c --- /dev/null +++ b/app/models/project_hook.rb @@ -0,0 +1,3 @@ +class ProjectHook < WebHook + belongs_to :project +end diff --git a/app/models/system_hook.rb b/app/models/system_hook.rb new file mode 100644 index 00000000000..8517d43a9de --- /dev/null +++ b/app/models/system_hook.rb @@ -0,0 +1,13 @@ +class SystemHook < WebHook + + def async_execute(data) + Resque.enqueue(SystemHookWorker, id, data) + end + + def self.all_hooks_fire(data) + SystemHook.all.each do |sh| + sh.async_execute data + end + end + +end diff --git a/app/models/user.rb b/app/models/user.rb index 4ead60a92db..ff27660a6ee 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -1,11 +1,12 @@ class User < ActiveRecord::Base + include Account - devise :database_authenticatable, :token_authenticatable, + devise :database_authenticatable, :token_authenticatable, :lockable, :recoverable, :rememberable, :trackable, :validatable, :omniauthable attr_accessible :email, :password, :password_confirmation, :remember_me, :bio, - :name, :projects_limit, :skype, :linkedin, :twitter, :dark_scheme, + :name, :projects_limit, :skype, :linkedin, :twitter, :dark_scheme, :theme_id, :force_random_password attr_accessor :force_random_password @@ -15,6 +16,11 @@ class User < ActiveRecord::Base has_many :my_own_projects, :class_name => "Project", :foreign_key => :owner_id has_many :keys, :dependent => :destroy + has_many :events, + :class_name => "Event", + :foreign_key => :author_id, + :dependent => :destroy + has_many :recent_events, :class_name => "Event", :foreign_key => :author_id, @@ -80,7 +86,8 @@ class User < ActiveRecord::Base def self.find_for_ldap_auth(omniauth_info) name = omniauth_info.name.force_encoding("utf-8") - email = omniauth_info.email.downcase + email = omniauth_info.email.downcase unless omniauth_info.email.nil? + raise OmniAuth::Error, "LDAP accounts must provide an email address" if email.nil? if @user = User.find_by_email(email) @user diff --git a/app/models/users_project.rb b/app/models/users_project.rb index 6ba72370931..4ff86290a92 100644 --- a/app/models/users_project.rb +++ b/app/models/users_project.rb @@ -68,7 +68,7 @@ class UsersProject < ActiveRecord::Base end def repo_access_human - "" + self.class.access_roles.invert[self.project_access] end end # == Schema Information diff --git a/app/models/web_hook.rb b/app/models/web_hook.rb index 26288476a6c..85d87898682 100644 --- a/app/models/web_hook.rb +++ b/app/models/web_hook.rb @@ -4,8 +4,6 @@ class WebHook < ActiveRecord::Base # HTTParty timeout default_timeout 10 - belongs_to :project - validates :url, presence: true, format: { @@ -14,9 +12,8 @@ class WebHook < ActiveRecord::Base def execute(data) WebHook.post(url, body: data.to_json, headers: { "Content-Type" => "application/json" }) - rescue - # There was a problem calling this web hook, let's forget about it. end + end # == Schema Information # diff --git a/app/observers/mailer_observer.rb b/app/observers/mailer_observer.rb index 880fd5026a4..451deccd14f 100644 --- a/app/observers/mailer_observer.rb +++ b/app/observers/mailer_observer.rb @@ -43,7 +43,7 @@ class MailerObserver < ActiveRecord::Observer end def new_merge_request(merge_request) - if merge_request.assignee != current_user + if merge_request.assignee && merge_request.assignee != current_user Notify.new_merge_request_email(merge_request.id).deliver end end diff --git a/app/observers/system_hook_observer.rb b/app/observers/system_hook_observer.rb new file mode 100644 index 00000000000..312cd2b3622 --- /dev/null +++ b/app/observers/system_hook_observer.rb @@ -0,0 +1,67 @@ +class SystemHookObserver < ActiveRecord::Observer + observe :user, :project, :users_project + + def after_create(model) + if model.kind_of? Project + SystemHook.all_hooks_fire({ + event_name: "project_create", + name: model.name, + path: model.path, + project_id: model.id, + owner_name: model.owner.name, + owner_email: model.owner.email, + created_at: model.created_at + }) + elsif model.kind_of? User + SystemHook.all_hooks_fire({ + event_name: "user_create", + name: model.name, + email: model.email, + created_at: model.created_at + }) + + elsif model.kind_of? UsersProject + SystemHook.all_hooks_fire({ + event_name: "user_add_to_team", + project_name: model.project.name, + project_path: model.project.path, + project_id: model.project_id, + user_name: model.user.name, + user_email: model.user.email, + project_access: model.repo_access_human, + created_at: model.created_at + }) + + end + end + + def after_destroy(model) + if model.kind_of? Project + SystemHook.all_hooks_fire({ + event_name: "project_destroy", + name: model.name, + path: model.path, + project_id: model.id, + owner_name: model.owner.name, + owner_email: model.owner.email, + }) + elsif model.kind_of? User + SystemHook.all_hooks_fire({ + event_name: "user_destroy", + name: model.name, + email: model.email + }) + + elsif model.kind_of? UsersProject + SystemHook.all_hooks_fire({ + event_name: "user_remove_from_team", + project_name: model.project.name, + project_path: model.project.path, + project_id: model.project_id, + user_name: model.user.name, + user_email: model.user.email, + project_access: model.repo_access_human + }) + end + end +end diff --git a/app/roles/account.rb b/app/roles/account.rb index afa1f8a347d..e86dc5939e0 100644 --- a/app/roles/account.rb +++ b/app/roles/account.rb @@ -55,4 +55,8 @@ module Account # Take only latest one events = events.recent.limit(1).first end + + def projects_with_events + projects.includes(:events).order("events.created_at DESC") + end end diff --git a/app/roles/git_push.rb b/app/roles/git_push.rb index b4c59472a5a..4ee7e62a69e 100644 --- a/app/roles/git_push.rb +++ b/app/roles/git_push.rb @@ -27,7 +27,7 @@ module GitPush true end - def execute_web_hooks(oldrev, newrev, ref, user) + def execute_hooks(oldrev, newrev, ref, user) ref_parts = ref.split('/') # Return if this is not a push to a branch (e.g. new commits) @@ -35,7 +35,7 @@ module GitPush data = post_receive_data(oldrev, newrev, ref, user) - web_hooks.each { |web_hook| web_hook.execute(data) } + hooks.each { |hook| hook.execute(data) } end def post_receive_data(oldrev, newrev, ref, user) @@ -97,7 +97,7 @@ module GitPush self.update_merge_requests(oldrev, newrev, ref, user) # Execute web hooks - self.execute_web_hooks(oldrev, newrev, ref, user) + self.execute_hooks(oldrev, newrev, ref, user) # Create satellite self.satellite.create unless self.satellite.exists? diff --git a/app/views/admin/hooks/_data_ex.html.erb b/app/views/admin/hooks/_data_ex.html.erb new file mode 100644 index 00000000000..652ee5aa56f --- /dev/null +++ b/app/views/admin/hooks/_data_ex.html.erb @@ -0,0 +1,66 @@ +<% data_ex_str = <<eos +1. Project created: +{ + "created_at": "2012-07-21T07:30:54Z", + "event_name": "project_create", + "name": "StoreCloud", + "owner_email": "johnsmith@gmail.com", + "owner_name": "John Smith", + "path": "storecloud", + "project_id": 74 +} + +2. Project destroyed: +{ + "event_name": "project_destroy", + "name": "Underscore", + "owner_email": "johnsmith@gmail.com", + "owner_name": "John Smith", + "path": "underscore", + "project_id": 73 +} + +3. New Team Member: +{ + "created_at": "2012-07-21T07:30:56Z", + "event_name": "user_add_to_team", + "project_access": "Master", + "project_id": 74, + "project_name": "StoreCloud", + "project_path": "storecloud", + "owner_email": "johnsmith@gmail.com", + "owner_name": "John Smith", +} + +4. Team Member Removed: +{ + "created_at": "2012-07-21T07:30:56Z", + "event_name": "user_remove_from_team", + "project_access": "Master", + "project_id": 74, + "project_name": "StoreCloud", + "project_path": "storecloud", + "owner_email": "johnsmith@gmail.com", + "owner_name": "John Smith", +} + +5. User created: +{ + "created_at": "2012-07-21T07:44:07Z", + "email": "js@gitlabhq.com", + "event_name": "user_create", + "name": "John Smith" +} + +6. User removed: +{ + "created_at": "2012-07-21T07:44:07Z", + "email": "js@gitlabhq.com", + "event_name": "user_destroy", + "name": "John Smith" +} + +eos +%> +<% js_lexer = Pygments::Lexer[:js] %> +<%= raw js_lexer.highlight(data_ex_str) %> diff --git a/app/views/admin/hooks/index.html.haml b/app/views/admin/hooks/index.html.haml new file mode 100644 index 00000000000..030e6136b1f --- /dev/null +++ b/app/views/admin/hooks/index.html.haml @@ -0,0 +1,39 @@ +.alert.alert-info + %span + Post receive hooks for binding events. + %br + Read more about system hooks + %strong #{link_to "here", help_system_hooks_path, :class => "vlink"} + += form_for @hook, :as => :hook, :url => admin_hooks_path do |f| + -if @hook.errors.any? + .alert-message.block-message.error + - @hook.errors.full_messages.each do |msg| + %p= msg + .clearfix + = f.label :url, "URL:" + .input + = f.text_field :url, :class => "text_field xxlarge" + + = f.submit "Add System Hook", :class => "btn primary" +%hr + +-if @hooks.any? + %h3 + Hooks + %small (#{@hooks.count}) + %br + %table.admin-table + %tr + %th URL + %th Method + %th + - @hooks.each do |hook| + %tr + %td + = link_to admin_hook_path(hook) do + %strong= hook.url + = link_to 'Test Hook', admin_hook_test_path(hook), :class => "btn small right" + %td POST + %td + = link_to 'Remove', admin_hook_path(hook), :confirm => 'Are you sure?', :method => :delete, :class => "danger btn small right" diff --git a/app/views/admin/logs/show.html.haml b/app/views/admin/logs/show.html.haml index 7963e18adcd..800d3bb288f 100644 --- a/app/views/admin/logs/show.html.haml +++ b/app/views/admin/logs/show.html.haml @@ -1,6 +1,9 @@ -%h4 - %i.icon-file - githost.log -%pre.logs - - Gitlab::Logger.read_latest.each do |line| - %span.log= line +.file_holder#README + .file_title + %i.icon-file + githost.log + .file_content.logs + %ol + - Gitlab::Logger.read_latest.each do |line| + %li + %p= line diff --git a/app/views/admin/mailer/preview.html.haml b/app/views/admin/mailer/preview.html.haml deleted file mode 100644 index 23ea7381cf5..00000000000 --- a/app/views/admin/mailer/preview.html.haml +++ /dev/null @@ -1,28 +0,0 @@ -%p This is page with preview for all system emails that are sent to user -%p Email previews built based on existing Project/Commit/Issue base - so some preview maybe unavailable unless object appear in system - -#accordion - %h3 - %a New user - %div - %iframe{ :src=> admin_mailer_preview_user_new_path, :width=>"100%", :height=>"350"} - %h3 - %a New issue - %div - %iframe{ :src=> admin_mailer_preview_issue_new_path, :width=>"100%", :height=>"350"} - %h3 - %a Commit note - %div - %iframe{ :src=> admin_mailer_preview_note_path(:type => "Commit"), :width=>"100%", :height=>"350"} - %h3 - %a Issue note - %div - %iframe{ :src=> admin_mailer_preview_note_path(:type => "Issue"), :width=>"100%", :height=>"350"} - %h3 - %a Wall note - %div - %iframe{ :src=> admin_mailer_preview_note_path(:type => "Wall"), :width=>"100%", :height=>"350"} - -:javascript - $(function() { - $("#accordion").accordion(); }); diff --git a/app/views/admin/projects/index.html.haml b/app/views/admin/projects/index.html.haml index 932fb37ddf6..7218eebb62a 100644 --- a/app/views/admin/projects/index.html.haml +++ b/app/views/admin/projects/index.html.haml @@ -13,8 +13,8 @@ %th Team Members %th Post Receive %th Last Commit - %th - %th + %th Edit + %th.cred Danger Zone! - @admin_projects.each do |project| %tr @@ -24,5 +24,5 @@ %td= check_box_tag :post_receive_file, 1, project.has_post_receive_file?, :disabled => true %td= last_commit(project) %td= link_to 'Edit', edit_admin_project_path(project), :id => "edit_#{dom_id(project)}", :class => "btn small" - %td= link_to 'Destroy', [:admin, project], :confirm => 'Are you sure?', :method => :delete, :class => "btn small danger" + %td.bgred= link_to 'Destroy', [:admin, project], :confirm => "REMOVE #{project.name}? Are you sure?", :method => :delete, :class => "btn small danger" = paginate @admin_projects, :theme => "admin" diff --git a/app/views/admin/users/_form.html.haml b/app/views/admin/users/_form.html.haml index bd2e136247a..c1955b321d5 100644 --- a/app/views/admin/users/_form.html.haml +++ b/app/views/admin/users/_form.html.haml @@ -50,7 +50,7 @@ .alert .clearfix - %p Give user ability to manage application. + %p Make the user a GitLab administrator. = f.label :admin, :class => "checkbox" do = f.check_box :admin %span Administrator @@ -59,11 +59,11 @@ - if @admin_user.blocked %span = link_to 'Unblock', unblock_admin_user_path(@admin_user), :method => :put, :class => "btn small" - This user is blocked and is not able to login GitLab + This user is blocked and is not able to login to GitLab - else %span = link_to 'Block', block_admin_user_path(@admin_user), :confirm => 'USER WILL BE BLOCKED! Are you sure?', :method => :put, :class => "btn small danger" - Blocked user will removed from all projects & will not be able to login to GitLab. + Blocked users will be removed from all projects & will not be able to login to GitLab. .actions = f.submit 'Save', :class => "btn primary" - if @admin_user.new_record? diff --git a/app/views/admin/users/index.html.haml b/app/views/admin/users/index.html.haml index 481bf37bb0b..5d5320db0e3 100644 --- a/app/views/admin/users/index.html.haml +++ b/app/views/admin/users/index.html.haml @@ -27,7 +27,7 @@ %th Projects %th Edit %th Blocked - %th + %th.cred Danger Zone! - @admin_users.each do |user| %tr @@ -41,6 +41,6 @@ = link_to 'Unblock', unblock_admin_user_path(user), :method => :put, :class => "btn small success" - else = link_to 'Block', block_admin_user_path(user), :confirm => 'USER WILL BE BLOCKED! Are you sure?', :method => :put, :class => "btn small danger" - %td= link_to 'Destroy', [:admin, user], :confirm => 'USER WILL BE REMOVED! Are you sure?', :method => :delete, :class => "btn small danger" + %td.bgred= link_to 'Destroy', [:admin, user], :confirm => "USER #{user.name} WILL BE REMOVED! Are you sure?", :method => :delete, :class => "btn small danger" = paginate @admin_users, :theme => "admin" diff --git a/app/views/commits/_commits.html.haml b/app/views/commits/_commits.html.haml index c2c9ca624b2..c3c7d49ce74 100644 --- a/app/views/commits/_commits.html.haml +++ b/app/views/commits/_commits.html.haml @@ -1,4 +1,6 @@ - @commits.group_by { |c| c.committed_date.to_date }.each do |day, commits| %div.ui-box - %h5= day.stamp("28 Aug, 2010") + %h5.small + %i.icon-calendar + = day.stamp("28 Aug, 2010") %ul.unstyled= render commits diff --git a/app/views/commits/_diffs.html.haml b/app/views/commits/_diffs.html.haml index 02a15633089..d51561d90f8 100644 --- a/app/views/commits/_diffs.html.haml +++ b/app/views/commits/_diffs.html.haml @@ -35,7 +35,13 @@ - if file.text? = render "commits/text_file", :diff => diff, :index => i - elsif file.image? - .diff_file_content_image{:class => image_diff_class(diff)} - %img{:src => "data:#{file.mime_type};base64,#{Base64.encode64(file.data)}"} + - if diff.renamed_file || diff.new_file || diff.deleted_file + .diff_file_content_image + %img{:class => image_diff_class(diff), :src => "data:#{file.mime_type};base64,#{Base64.encode64(file.data)}"} + - else + - old_file = (@commit.prev_commit.tree / diff.old_path) + .diff_file_content_image.img_compared + %img{:class => "diff_image_removed", :src => "data:#{file.mime_type};base64,#{Base64.encode64(old_file.data)}"} + %img{:class => "diff_image_added", :src => "data:#{file.mime_type};base64,#{Base64.encode64(file.data)}"} - else %p.nothing_here_message No preview for this file type diff --git a/app/views/commits/_head.html.haml b/app/views/commits/_head.html.haml index 8e9195c5b50..453ca4eac12 100644 --- a/app/views/commits/_head.html.haml +++ b/app/views/commits/_head.html.haml @@ -13,12 +13,12 @@ %li{:class => "#{branches_tab_class}"} = link_to project_repository_path(@project) do Branches - %span.number= @project.repo.branch_count + %span.badge= @project.repo.branch_count %li{:class => "#{'active' if current_page?(tags_project_repository_path(@project)) }"} = link_to tags_project_repository_path(@project) do Tags - %span.number= @project.repo.tag_count + %span.badge= @project.repo.tag_count - if current_page?(project_commits_path(@project)) && current_user.private_token diff --git a/app/views/commits/compare.html.haml b/app/views/commits/compare.html.haml index c02263296f4..66ed8dad595 100644 --- a/app/views/commits/compare.html.haml +++ b/app/views/commits/compare.html.haml @@ -20,7 +20,7 @@ = "..." = text_field_tag :to, params[:to], :placeholder => "aa8b4ef", :class => "xlarge" .actions - = submit_tag "Compare", :class => "btn primary" + = submit_tag "Compare", :class => "btn btn-primary" - unless @commits.empty? diff --git a/app/views/dashboard/index.atom.builder b/app/views/dashboard/index.atom.builder index 706b808ee43..fa3bfade28b 100644 --- a/app/views/dashboard/index.atom.builder +++ b/app/views/dashboard/index.atom.builder @@ -8,17 +8,10 @@ xml.feed "xmlns" => "http://www.w3.org/2005/Atom", "xmlns:media" => "http://sear @events.each do |event| if event.allowed? + event = EventDecorator.decorate(event) xml.entry do - if event.issue? - event_link = project_issue_url(event.project, event.issue) - event_title = event.issue_title - elsif event.merge_request? - event_link = project_merge_request_url(event.project, event.merge_request) - event_title = event.merge_request_title - elsif event.push? - event_link = project_commits_url(event.project, :ref => event.ref_name) - event_title = event.ref_name - end + event_link = event.feed_url + event_title = event.feed_title xml.id "tag:#{request.host},#{event.created_at.strftime("%Y-%m-%d")}:#{event.id}" xml.link :href => event_link diff --git a/app/views/dashboard/index.html.haml b/app/views/dashboard/index.html.haml index b38544509b2..e1d7781927a 100644 --- a/app/views/dashboard/index.html.haml +++ b/app/views/dashboard/index.html.haml @@ -10,9 +10,10 @@ add new key to your profile - if @events.any? - = render @events + .content_list= render @events - else %h4.nothing_here_message Projects activity will be displayed here + .loading.hide .side = render "events/event_last_push", :event => @last_push .projects_box @@ -54,3 +55,7 @@ New Project » - else If you will be added to project - it will be displayed here + + +:javascript + $(function(){ Pager.init(20); }); diff --git a/app/views/dashboard/index.js.haml b/app/views/dashboard/index.js.haml index aa038e75928..7e5a148e5ef 100644 --- a/app/views/dashboard/index.js.haml +++ b/app/views/dashboard/index.js.haml @@ -1,2 +1,2 @@ :plain - $(".projects .activities").append("#{escape_javascript(render(@events))}"); + Pager.append(#{@events.count}, "#{escape_javascript(render(@events))}"); diff --git a/app/views/events/_event_issue.html.haml b/app/views/events/_event_issue.html.haml index 13fb20cd379..4293be8204e 100644 --- a/app/views/events/_event_issue.html.haml +++ b/app/views/events/_event_issue.html.haml @@ -1,7 +1,7 @@ = image_tag gravatar_icon(event.author_email), :class => "avatar" %strong #{event.author_name} -%span.event_label= event.action_name - issue +%span.event_label{:class => event.action_name}= event.action_name +issue = link_to project_issue_path(event.project, event.issue) do %strong= truncate event.issue_title at diff --git a/app/views/events/_event_last_push.html.haml b/app/views/events/_event_last_push.html.haml index fb4a728ed3e..212ef817e60 100644 --- a/app/views/events/_event_last_push.html.haml +++ b/app/views/events/_event_last_push.html.haml @@ -5,12 +5,9 @@ %span Your pushed to = event.ref_type = link_to project_commits_path(event.project, :ref => event.ref_name) do - %strong= event.ref_name + %strong= truncate(event.ref_name, :length => 28) at %strong= link_to event.project.name, event.project - %span.cgray - = time_ago_in_words(event.created_at) - ago. = link_to new_mr_path_from_push_event(event), :title => "New Merge Request", :class => "btn very_small primary" do Create Merge Request diff --git a/app/views/events/_event_merge_request.html.haml b/app/views/events/_event_merge_request.html.haml index e59a74b9554..774921a7f2a 100644 --- a/app/views/events/_event_merge_request.html.haml +++ b/app/views/events/_event_merge_request.html.haml @@ -2,8 +2,8 @@ .event_icon= image_tag "event_mr_merged.png" = image_tag gravatar_icon(event.author_email), :class => "avatar" %strong #{event.author_name} -%span.event_label= event.action_name - merge request +%span.event_label{:class => event.action_name}= event.action_name +merge request = link_to project_merge_request_path(event.project, event.merge_request) do %strong= truncate event.merge_request_title at diff --git a/app/views/events/_event_push.html.haml b/app/views/events/_event_push.html.haml index 3aadd226614..59d8962bb16 100644 --- a/app/views/events/_event_push.html.haml +++ b/app/views/events/_event_push.html.haml @@ -2,7 +2,7 @@ .event_icon= image_tag "event_push.png" = image_tag gravatar_icon(event.author_email), :class => "avatar" %strong #{event.author_name} - %span.event_label= event.push_action_name + %span.event_label.pushed= event.push_action_name = event.ref_type = link_to project_commits_path(event.project, :ref => event.ref_name) do %strong= event.ref_name diff --git a/app/views/help/api.html.haml b/app/views/help/api.html.haml new file mode 100644 index 00000000000..4964c1bbd87 --- /dev/null +++ b/app/views/help/api.html.haml @@ -0,0 +1,41 @@ +%h3 API +.back_link + = link_to help_path do + ← to index +%hr + +%ol + %li + %a{:href => "#README"} README + %li + %a{:href => "#projects"} Projects + %li + %a{:href => "#users"} Users + +.file_holder#README + .file_title + %i.icon-file + README + .file_content.wiki + = preserve do + = markdown File.read(Rails.root.join("doc", "api", "README.md")) + +%br + +.file_holder#projects + .file_title + %i.icon-file + Projects + .file_content.wiki + = preserve do + = markdown File.read(Rails.root.join("doc", "api", "projects.md")) + +%br + +.file_holder#users + .file_title + %i.icon-file + Users + .file_content.wiki + = preserve do + = markdown File.read(Rails.root.join("doc", "api", "users.md")) diff --git a/app/views/help/index.html.haml b/app/views/help/index.html.haml index 25b9e3e5208..e9602e33037 100644 --- a/app/views/help/index.html.haml +++ b/app/views/help/index.html.haml @@ -22,3 +22,9 @@ %li %h5= link_to "Web Hooks", help_web_hooks_path + + %li + %h5= link_to "System Hooks", help_system_hooks_path + + %li + %h5= link_to "API", help_api_path diff --git a/app/views/help/system_hooks.html.haml b/app/views/help/system_hooks.html.haml new file mode 100644 index 00000000000..2088208ad47 --- /dev/null +++ b/app/views/help/system_hooks.html.haml @@ -0,0 +1,13 @@ +%h3 System hooks +.back_link + = link_to :back do + ← back +%hr + +%p.slead + Your Gitlab instance can perform HTTP POST request on next event: create_project, delete_project, create_user, delete_user, change_team_member. + %br + System Hooks can be used for logging or change information in LDAP server. + %br +%h5 Hooks request example: += render "admin/hooks/data_ex" diff --git a/app/views/issues/_issues.html.haml b/app/views/issues/_issues.html.haml index a20df176afc..17141cc453c 100644 --- a/app/views/issues/_issues.html.haml +++ b/app/views/issues/_issues.html.haml @@ -6,7 +6,9 @@ .row .span7= paginate @issues, :remote => true, :theme => "gitlab" .span3.right - %span.cgray.right #{@issues.total_count} issues for this filter + %span.cgray.right + %span.issue_counter #{@issues.total_count} + issues for this filter - else %li %h4.nothing_here_message Nothing to show here diff --git a/app/views/issues/_show.html.haml b/app/views/issues/_show.html.haml index 03524901c99..e12c3c1a99c 100644 --- a/app/views/issues/_show.html.haml +++ b/app/views/issues/_show.html.haml @@ -12,9 +12,9 @@ = issue.notes.count - if can? current_user, :modify_issue, issue - if issue.closed - = link_to 'Reopen', project_issue_path(issue.project, issue, :issue => {:closed => false }, :status_only => true), :method => :put, :class => "btn small grouped", :remote => true + = link_to 'Reopen', project_issue_path(issue.project, issue, :issue => {:closed => false }, :status_only => true), :method => :put, :class => "btn small grouped reopen_issue", :remote => true - else - = link_to 'Resolve', project_issue_path(issue.project, issue, :issue => {:closed => true }, :status_only => true), :method => :put, :class => "success btn small grouped", :remote => true + = link_to 'Resolve', project_issue_path(issue.project, issue, :issue => {:closed => true }, :status_only => true), :method => :put, :class => "success btn small grouped close_issue", :remote => true = link_to edit_project_issue_path(issue.project, issue), :class => "btn small edit-issue-link", :remote => true do %i.icon-edit Edit @@ -35,6 +35,4 @@ - if issue.upvotes > 0 - %span.badge.badge-success= "+#{issue.upvotes}" - - + %span.badge.badge-success= "+#{issue.upvotes}"
\ No newline at end of file diff --git a/app/views/issues/index.html.haml b/app/views/issues/index.html.haml index 7328fa88812..fb8b9f8ee8e 100644 --- a/app/views/issues/index.html.haml +++ b/app/views/issues/index.html.haml @@ -2,7 +2,7 @@ .issues_content %h3.page_title Issues - %small (#{@issues.total_count}) + %small (<span class=issue_counter>#{@issues.total_count}</span>) .right .span5 - if can? current_user, :write_issue, @project @@ -45,4 +45,4 @@ :javascript $(function(){ issuesPage(); - }) + })
\ No newline at end of file diff --git a/app/views/keys/new.html.haml b/app/views/keys/new.html.haml index 277936c6743..02e782b9f85 100644 --- a/app/views/keys/new.html.haml +++ b/app/views/keys/new.html.haml @@ -1,4 +1,4 @@ -%h3 New key +%h3.page_title New key %hr = render 'form' @@ -11,4 +11,4 @@ if( key_mail && key_mail.length > 0 && title.val() == '' ){ $('#key_title').val( key_mail ); } - });
\ No newline at end of file + }); diff --git a/app/views/layouts/_project_menu.html.haml b/app/views/layouts/_project_menu.html.haml index 62279bb058d..3f58fc5a664 100644 --- a/app/views/layouts/_project_menu.html.haml +++ b/app/views/layouts/_project_menu.html.haml @@ -17,14 +17,14 @@ %li{:class => tab_class(:issues)} = link_to project_issues_filter_path(@project) do Issues - %span.count= @project.issues.opened.count + %span.count.issue_counter= @project.issues.opened.count - if @project.repo_exists? - if @project.merge_requests_enabled %li{:class => tab_class(:merge_requests)} = link_to project_merge_requests_path(@project) do Merge Requests - %span.count= @project.merge_requests.opened.count + %span.count.merge_counter= @project.merge_requests.opened.count - if @project.wall_enabled %li{:class => tab_class(:wall)} diff --git a/app/views/layouts/admin.html.haml b/app/views/layouts/admin.html.haml index 8de25821ee7..69304edef3a 100644 --- a/app/views/layouts/admin.html.haml +++ b/app/views/layouts/admin.html.haml @@ -15,7 +15,7 @@ %li{:class => tab_class(:admin_logs)} = link_to "Logs", admin_logs_path %li{:class => tab_class(:admin_emails)} - = link_to "Emails", admin_emails_path + = link_to "Hooks", admin_hooks_path %li{:class => tab_class(:admin_resque)} = link_to "Resque", admin_resque_path diff --git a/app/views/layouts/devise.html.haml b/app/views/layouts/devise_layout.html.haml index c293734b368..c293734b368 100644 --- a/app/views/layouts/devise.html.haml +++ b/app/views/layouts/devise_layout.html.haml diff --git a/app/views/layouts/profile.html.haml b/app/views/layouts/profile.html.haml index dfe48d68240..957a68bf482 100644 --- a/app/views/layouts/profile.html.haml +++ b/app/views/layouts/profile.html.haml @@ -12,16 +12,17 @@ %li{:class => tab_class(:password)} = link_to "Password", profile_password_path + %li{:class => tab_class(:ssh_keys)} + = link_to keys_path do + SSH Keys + %span.count= current_user.keys.count + %li{:class => tab_class(:token)} = link_to "Token", profile_token_path %li{:class => tab_class(:design)} = link_to "Design", profile_design_path - %li{:class => tab_class(:ssh_keys)} - = link_to keys_path do - SSH Keys - %span.count= current_user.keys.count .content = yield diff --git a/app/views/merge_requests/_form.html.haml b/app/views/merge_requests/_form.html.haml index d69faa142d5..4f20a06fd25 100644 --- a/app/views/merge_requests/_form.html.haml +++ b/app/views/merge_requests/_form.html.haml @@ -5,7 +5,8 @@ - @merge_request.errors.full_messages.each do |msg| %li= msg - %h3.padded.cgray 1. Select Branches + %h4.cdark 1. Select Branches + %br .row .span6 @@ -30,14 +31,21 @@ .bottom_commit .mr_target_commit - %h3.padded.cgray 2. Fill info + %h4.cdark 2. Fill info + .clearfix - = f.label :assignee_id, "Assign to", :class => "control-label" - .controls= f.select(:assignee_id, @project.users.all.collect {|p| [ p.name, p.id ] }, { :include_blank => "Select user" }, :style => "width:250px") + .main_box + .top_box_content + = f.label :title do + %strong= "Title *" + .input= f.text_field :title, :class => "input-xxlarge pad", :maxlength => 255, :rows => 5 + .middle_box_content + = f.label :assignee_id do + %i.icon-user + Assign to + .input= f.select(:assignee_id, @project.users.all.collect {|p| [ p.name, p.id ] }, { :include_blank => "Select user" }, :style => "width:250px") .control-group - = f.label :title, :class => "control-label" - .controls= f.text_field :title, :class => "input-xxlarge pad", :maxlength => 255, :rows => 5 .form-actions = f.submit 'Save', :class => "btn-primary btn" diff --git a/app/views/merge_requests/_merge_request.html.haml b/app/views/merge_requests/_merge_request.html.haml index d9634a4fcc8..b9a005e08be 100644 --- a/app/views/merge_requests/_merge_request.html.haml +++ b/app/views/merge_requests/_merge_request.html.haml @@ -15,12 +15,14 @@ → = merge_request.target_branch = image_tag gravatar_icon(merge_request.author_email), :class => "avatar" + + = link_to project_merge_request_path(merge_request.project, merge_request) do + %p.row_title= truncate(merge_request.title, :length => 80) + %span.update-author - %strong= merge_request.author_name - authored + %small.cdark= "##{merge_request.id}" + authored by #{merge_request.author_name} = time_ago_in_words(merge_request.created_at) ago - if merge_request.upvotes > 0 %span.badge.badge-success= "+#{merge_request.upvotes}" - = link_to project_merge_request_path(merge_request.project, merge_request) do - %p.row_title= truncate(merge_request.title, :length => 80) diff --git a/app/views/merge_requests/edit.html.haml b/app/views/merge_requests/edit.html.haml index 9e4f9327cdc..eee148994d7 100644 --- a/app/views/merge_requests/edit.html.haml +++ b/app/views/merge_requests/edit.html.haml @@ -1,4 +1,4 @@ -%h3 +%h3.page_title = "Edit merge request #{@merge_request.id}" %hr = render 'form' diff --git a/app/views/merge_requests/new.html.haml b/app/views/merge_requests/new.html.haml index efafa45d758..594089995ea 100644 --- a/app/views/merge_requests/new.html.haml +++ b/app/views/merge_requests/new.html.haml @@ -1,3 +1,3 @@ -%h3 New Merge Request +%h3.page_title New Merge Request %hr = render 'form' diff --git a/app/views/merge_requests/show/_commits.html.haml b/app/views/merge_requests/show/_commits.html.haml index 78fe1a062d3..d10e8fd597a 100644 --- a/app/views/merge_requests/show/_commits.html.haml +++ b/app/views/merge_requests/show/_commits.html.haml @@ -1,6 +1,8 @@ - if @commits.present? .ui-box - %h5 Commits (#{@commits.count}) + %h5 + %i.icon-list + Commits (#{@commits.count}) .merge-request-commits - if @commits.count > 8 %ul.first_mr_commits.unstyled diff --git a/app/views/merge_requests/show/_mr_box.html.haml b/app/views/merge_requests/show/_mr_box.html.haml index 3027719d94d..b542dac98e0 100644 --- a/app/views/merge_requests/show/_mr_box.html.haml +++ b/app/views/merge_requests/show/_mr_box.html.haml @@ -13,9 +13,10 @@ = image_tag gravatar_icon(@merge_request.author_email), :width => 16, :class => "lil_av" %strong.author= link_to_merge_request_author(@merge_request) - %cite.cgray and currently assigned to - = image_tag gravatar_icon(@merge_request.assignee_email), :width => 16, :class => "lil_av" - %strong.author= link_to_merge_request_assignee(@merge_request) + - if @merge_request.assignee + %cite.cgray and currently assigned to + = image_tag gravatar_icon(@merge_request.assignee_email), :width => 16, :class => "lil_av" + %strong.author= link_to_merge_request_assignee(@merge_request) - if @merge_request.closed diff --git a/app/views/notes/_form.html.haml b/app/views/notes/_form.html.haml index 03774d160b9..f5aa1495796 100644 --- a/app/views/notes/_form.html.haml +++ b/app/views/notes/_form.html.haml @@ -32,4 +32,4 @@ %span Any file less than 10 MB - = f.submit 'Add Comment', :class => "btn primary", :id => "submit_note" + = f.submit 'Add Comment', :class => "btn primary submit_note", :id => "submit_note" diff --git a/app/views/notes/_per_line_form.html.haml b/app/views/notes/_per_line_form.html.haml index 94c558029d2..8beaf9b5e0c 100644 --- a/app/views/notes/_per_line_form.html.haml +++ b/app/views/notes/_per_line_form.html.haml @@ -24,7 +24,7 @@ = check_box_tag :notify_author, 1 , @note.noteable_type == "Commit" %span Commit author .actions - = f.submit 'Add note', :class => "btn primary", :id => "submit_note" + = f.submit 'Add note', :class => "btn primary submit_note", :id => "submit_note" = link_to "Close", "#", :class => "btn hide-button" :javascript diff --git a/app/views/notes/_reply_button.html.haml b/app/views/notes/_reply_button.html.haml index f03ba4d7018..db6b35b7ada 100644 --- a/app/views/notes/_reply_button.html.haml +++ b/app/views/notes/_reply_button.html.haml @@ -1,3 +1,4 @@ %tr.line_notes_row.reply %td{:colspan => 3} + %i.icon-comment = link_to "Reply", "#", :class => "line_note_reply_link", "line_code" => line_code, :title => "Add note for this line" diff --git a/app/views/refs/_tree.html.haml b/app/views/refs/_tree.html.haml index ee2f278693c..ba0bd69116b 100644 --- a/app/views/refs/_tree.html.haml +++ b/app/views/refs/_tree.html.haml @@ -13,7 +13,7 @@ = render :partial => "refs/tree_file", :locals => { :name => tree.name, :content => tree.data, :file => tree } - else - contents = tree.contents - %table#tree-slider.bordered-table.table + %table#tree-slider.bordered-table.table{:class => "table_#{@hex_path}" } %thead %th Name %th Last Update @@ -29,34 +29,39 @@ %td %td + - index = 0 - contents.select{ |i| i.is_a?(Grit::Tree)}.each do |content| - = render :partial => "refs/tree_item", :locals => { :content => content } + = render :partial => "refs/tree_item", :locals => { :content => content, :index => (index += 1) } - contents.select{ |i| i.is_a?(Grit::Blob)}.each do |content| - = render :partial => "refs/tree_item", :locals => { :content => content } + = render :partial => "refs/tree_item", :locals => { :content => content, :index => (index += 1) } - contents.select{ |i| i.is_a?(Grit::Submodule)}.each do |content| - = render :partial => "refs/submodule_item", :locals => { :content => content } + = render :partial => "refs/submodule_item", :locals => { :content => content, :index => (index += 1) } - if content = contents.select{ |c| c.is_a?(Grit::Blob) and c.name =~ /^readme/i }.first - #tree-readme-holder - %h3= content.name - .readme + .file_holder#README + .file_title + %i.icon-file + = content.name + .file_content.wiki - if content.name =~ /\.(md|markdown)$/i = preserve do = markdown(content.data) - else = simple_format(content.data) -- if params[:path] - - history_path = tree_file_project_ref_path(@project, @ref, params[:path]) -- else - - history_path = tree_project_ref_path(@project, @ref) :javascript $(function(){ $('select#branch').selectmenu({style:'popup', width:200}); $('select#tag').selectmenu({style:'popup', width:200}); $('.project-refs-select').chosen(); - history.pushState({ path: this.path }, '', "#{history_path}") + history.pushState({ path: this.path }, '', "#{@history_path}"); + + }); + + // Load last commit log for each file in tree + $(window).load(function(){ + ajaxGet('#{@logs_path}'); }); diff --git a/app/views/refs/_tree_commit.html.haml b/app/views/refs/_tree_commit.html.haml new file mode 100644 index 00000000000..1f2524a4c3a --- /dev/null +++ b/app/views/refs/_tree_commit.html.haml @@ -0,0 +1,3 @@ +- if tm + %strong= link_to "[#{tm.user_name}]", project_team_member_path(@project, tm) += link_to truncate(content_commit.safe_message, :length => tm ? 30 : 50), project_commit_path(@project, content_commit.id), :class => "tree-commit-link" diff --git a/app/views/refs/_tree_file.html.haml b/app/views/refs/_tree_file.html.haml index ee56ab36194..d45a03df8aa 100644 --- a/app/views/refs/_tree_file.html.haml +++ b/app/views/refs/_tree_file.html.haml @@ -1,5 +1,5 @@ -.view_file - .view_file_header +.file_holder + .file_title %i.icon-file %span.file_name = name @@ -10,26 +10,28 @@ = link_to "blame", blame_file_project_ref_path(@project, @ref, :path => params[:path]), :class => "btn very_small" - if file.text? - if name =~ /\.(md|markdown)$/i - #tree-readme-holder - .readme - = preserve do - = markdown(file.data) + .file_content.wiki + = preserve do + = markdown(file.data) - else - .view_file_content + .file_content.code - unless file.empty? %div{:class => current_user.dark_scheme ? "black" : "white"} = preserve do = raw file.colorize(options: { linenos: 'True'}) - else %h4.nothing_here_message Empty file + - elsif file.image? - .view_file_content_image + .file_content.image_file %img{ :src => "data:#{file.mime_type};base64,#{Base64.encode64(file.data)}"} + - else - %center - = link_to blob_project_ref_path(@project, @ref, :path => params[:path]) do - %div.padded - %br - = image_tag "download.png", :width => 64 - %h3 - Download (#{file.mb_size}) + .file_content.blob_file + %center + = link_to blob_project_ref_path(@project, @ref, :path => params[:path]) do + %div.padded + %br + = image_tag "download.png", :width => 64 + %h3 + Download (#{file.mb_size}) diff --git a/app/views/refs/_tree_item.html.haml b/app/views/refs/_tree_item.html.haml index fe2f7293347..4ce16b22ddd 100644 --- a/app/views/refs/_tree_item.html.haml +++ b/app/views/refs/_tree_item.html.haml @@ -1,23 +1,11 @@ -- file = params[:path] ? File.join(params[:path], content.name) : content.name -- content_commit = @project.commits(@commit.id, file, 1).last -- return unless content_commit -%tr{ :class => "tree-item", :url => tree_file_project_ref_path(@project, @ref, file) } +- file = tree_full_path(content) +%tr{ :class => "tree-item #{tree_hex_class(content)}", :url => tree_file_project_ref_path(@project, @ref, file) } %td.tree-item-file-name - - if content.is_a?(Grit::Blob) - - if content.text? - = image_tag "file_txt.png" - - elsif content.image? - = image_tag "file_img.png" - - else - = image_tag "file_bin.png" - - else - = image_tag "file_dir.png" + = tree_icon(content) = link_to truncate(content.name, :length => 40), tree_file_project_ref_path(@project, @ref || @commit.id, file), :remote => :true - %td.cgray - = time_ago_in_words(content_commit.committed_date) - ago - %td.commit - - tm = @project.team_member_by_name_or_email(content_commit.author_email, content_commit.author_name) - - if tm - %strong= link_to "[#{tm.user_name}]", project_team_member_path(@project, tm) - = link_to truncate(content_commit.safe_message, :length => tm ? 30 : 50), project_commit_path(@project, content_commit.id), :class => "tree-commit-link" + %td.tree_time_ago.cgray + - if index == 1 + %span.log_loading + Loading commit data.. + = image_tag "ajax_loader_tree.gif", :width => 14 + %td.tree_commit diff --git a/app/views/refs/blame.html.haml b/app/views/refs/blame.html.haml index 7307d557a30..6a86b91fe74 100644 --- a/app/views/refs/blame.html.haml +++ b/app/views/refs/blame.html.haml @@ -11,8 +11,8 @@ %li= link .clear - .view_file.blame_file - .view_file_header + .file_holder + .file_title %i.icon-file %span.file_name = @tree.name @@ -21,7 +21,7 @@ = link_to "raw", blob_project_ref_path(@project, @ref, :path => params[:path]), :class => "btn very_small", :target => "_blank" = link_to "history", project_commits_path(@project, :path => params[:path], :ref => @ref), :class => "btn very_small" = link_to "source", tree_file_project_ref_path(@project, @ref, :path => params[:path]), :class => "btn very_small" - .view_file_content + .file_content.blame %table - @blame.each do |commit, lines| - commit = Commit.new(commit) @@ -29,7 +29,7 @@ %td.author = image_tag gravatar_icon(commit.author_email, 16) = commit.author_name - %td.commit + %td.blame_commit = link_to project_commit_path(@project, :id => commit.id) do %code= commit.id.to_s[0..10] @@ -37,8 +37,7 @@ %td.lines = preserve do %pre - - lines.each do |line| - = line + = Gitlab::Encode.utf8 lines.join("\n") :javascript $(function(){ diff --git a/app/views/refs/logs_tree.js.haml b/app/views/refs/logs_tree.js.haml new file mode 100644 index 00000000000..402f5aa72bc --- /dev/null +++ b/app/views/refs/logs_tree.js.haml @@ -0,0 +1,9 @@ +- @logs.each do |content_data| + - file_name = content_data[:file_name] + - content_commit = content_data[:commit] + - tm = @project.team_member_by_name_or_email(content_commit.author_email, content_commit.author_name) + + :plain + var row = $("table.table_#{@hex_path} tr.file_#{hexdigest(file_name)}"); + row.find("td.tree_time_ago").html('#{escape_javascript(time_ago_in_words(content_commit.committed_date))} ago'); + row.find("td.tree_commit").html('#{escape_javascript(render("tree_commit", :tm => tm, :content_commit => content_commit))}'); diff --git a/app/views/refs/tree.js.haml b/app/views/refs/tree.js.haml index f4f28adc369..9cf55057a6a 100644 --- a/app/views/refs/tree.js.haml +++ b/app/views/refs/tree.js.haml @@ -1,4 +1,10 @@ :plain + // Load Files list $("#tree-holder").html("#{escape_javascript(render(:partial => "tree", :locals => {:repo => @repo, :commit => @commit, :tree => @tree}))}"); $("#tree-content-holder").show("slide", { direction: "right" }, 150); $('.project-refs-form #path').val("#{params[:path]}"); + + // Load last commit log for each file in tree + $('#tree-slider').waitForImages(function() { + ajaxGet('#{@logs_path}'); + }); diff --git a/app/views/snippets/show.html.haml b/app/views/snippets/show.html.haml index c934e262488..b266e4d2156 100644 --- a/app/views/snippets/show.html.haml +++ b/app/views/snippets/show.html.haml @@ -7,16 +7,14 @@ = link_to "Edit", edit_project_snippet_path(@project, @snippet), :class => "btn small right" %br -#tree-holder - #tree-content-holder - .view_file - .view_file_header - %i.icon-file - %strong= @snippet.file_name - %span.options - = link_to "raw", raw_project_snippet_path(@project, @snippet), :class => "btn very_small", :target => "_blank" - .view_file_content - %div{:class => current_user.dark_scheme ? "black" : ""} - = raw @snippet.colorize(options: { linenos: 'True'}) +.file_holder + .file_title + %i.icon-file + %strong= @snippet.file_name + %span.options + = link_to "raw", raw_project_snippet_path(@project, @snippet), :class => "btn very_small", :target => "_blank" + .file_content.code + %div{:class => current_user.dark_scheme ? "black" : ""} + = raw @snippet.colorize(options: { linenos: 'True'}) = render "notes/notes", :tid => @snippet.id, :tt => "snippet" diff --git a/app/workers/system_hook_worker.rb b/app/workers/system_hook_worker.rb new file mode 100644 index 00000000000..ca154136b97 --- /dev/null +++ b/app/workers/system_hook_worker.rb @@ -0,0 +1,7 @@ +class SystemHookWorker + @queue = :system_hook + + def self.perform(hook_id, data) + SystemHook.find(hook_id).execute data + end +end diff --git a/config/application.rb b/config/application.rb index 3585c4b0a87..937262237e9 100644 --- a/config/application.rb +++ b/config/application.rb @@ -23,7 +23,7 @@ module Gitlab # config.plugins = [ :exception_notification, :ssl_requirement, :all ] # Activate observers that should always be running. - config.active_record.observers = :mailer_observer, :activity_observer, :project_observer, :key_observer, :issue_observer, :user_observer + config.active_record.observers = :mailer_observer, :activity_observer, :project_observer, :key_observer, :issue_observer, :user_observer, :system_hook_observer # Set Time.zone default to the specified zone and make Active Record auto-convert to this zone. # Run "rake -D time" for a list of tasks for finding time zone names. Default is UTC. diff --git a/config/gitlab.yml.example b/config/gitlab.yml.example index 12c28675139..1818f2c0d01 100644 --- a/config/gitlab.yml.example +++ b/config/gitlab.yml.example @@ -21,6 +21,8 @@ email: # Like default project limit for user etc app: default_projects_limit: 10 + # backup_path: "/vol/backups" # default: Rails.root + backups/ + # backup_keep_time: 604800 # default: 0 (forever) (in seconds) # diff --git a/config/initializers/1_settings.rb b/config/initializers/1_settings.rb index 8b9ed8aebd6..5c5987a8857 100644 --- a/config/initializers/1_settings.rb +++ b/config/initializers/1_settings.rb @@ -95,11 +95,21 @@ class Settings < Settingslogic end def gitolite_admin_uri - git['admin_uri'] || 'git@localhost:gitolite-admin' + git_host['admin_uri'] || 'git@localhost:gitolite-admin' end def default_projects_limit app['default_projects_limit'] || 10 end + + def backup_path + t = app['backup_path'] || "backups/" + t = /^\//.match(t) ? t : File.join(Rails.root + t) + t + end + + def backup_keep_time + app['backup_keep_time'] || 0 + end end end diff --git a/config/initializers/devise.rb b/config/initializers/devise.rb index cb1ae0ac0be..54011ba5ea3 100644 --- a/config/initializers/devise.rb +++ b/config/initializers/devise.rb @@ -93,10 +93,6 @@ Devise.setup do |config| # If true, extends the user's remember period when remembered via cookie. # config.extend_remember_period = false - # If true, uses the password salt as remember token. This should be turned - # to false if you are not using database authenticatable. - config.use_salt_as_remember_token = true - # Options to be passed to the created cookie. For instance, you can set # :secure => true in order to force SSL only cookies. # config.cookie_options = {} @@ -119,7 +115,7 @@ Devise.setup do |config| # Defines which strategy will be used to lock an account. # :failed_attempts = Locks an account after a number of failed attempts to sign in. # :none = No lock strategy. You should handle locking by yourself. - # config.lock_strategy = :failed_attempts + config.lock_strategy = :failed_attempts # Defines which key will be used when locking and unlocking an account # config.unlock_keys = [ :email ] @@ -129,14 +125,14 @@ Devise.setup do |config| # :time = Re-enables login after a certain amount of time (see :unlock_in below) # :both = Enables both strategies # :none = No unlock strategy. You should handle unlocking by yourself. - # config.unlock_strategy = :both + config.unlock_strategy = :time # Number of authentication tries before locking an account if lock_strategy # is failed attempts. - # config.maximum_attempts = 20 + config.maximum_attempts = 10 # Time interval to unlock the account if :time is enabled as unlock_strategy. - # config.unlock_in = 1.hour + config.unlock_in = 10.minutes # ==> Configuration for :recoverable # @@ -160,9 +156,9 @@ Devise.setup do |config| # Defines name of the authentication token params key config.token_authentication_key = :private_token - # If true, authentication through token does not store user in session and needs + # Authentication through token does not store user in session and needs # to be supplied on each request. Useful if you are using the token as API token. - config.stateless_token = true + config.skip_session_storage << :token_auth # ==> Scopes configuration # Turn scoped views on. Before rendering "sessions/new", it will first check for diff --git a/config/locales/devise.en.yml b/config/locales/devise.en.yml index b18263510f8..a78cb6b670b 100644 --- a/config/locales/devise.en.yml +++ b/config/locales/devise.en.yml @@ -35,13 +35,11 @@ en: confirmed: 'Your account was successfully confirmed. You are now signed in.' registrations: signed_up: 'Welcome! You have signed up successfully.' - inactive_signed_up: 'You have signed up successfully. However, we could not sign you in because your account is %{reason}.' updated: 'You updated your account successfully.' destroyed: 'Bye! Your account was successfully cancelled. We hope to see you again soon.' - reasons: - inactive: 'inactive' - unconfirmed: 'unconfirmed' - locked: 'locked' + signed_up_but_unconfirmed: 'A message with a confirmation link has been sent to your email address. Please open the link to activate your account.' + signed_up_but_inactive: 'You have signed up successfully. However, we could not sign you in because your account is not yet activated.' + signed_up_but_locked: 'You have signed up successfully. However, we could not sign you in because your account is locked.' unlocks: send_instructions: 'You will receive an email with instructions about how to unlock your account in a few minutes.' unlocked: 'Your account was successfully unlocked. You are now signed in.' diff --git a/config/routes.rb b/config/routes.rb index af99109883f..dea4df46a30 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -26,7 +26,9 @@ Gitlab::Application.routes.draw do get 'help' => 'help#index' get 'help/permissions' => 'help#permissions' get 'help/workflow' => 'help#workflow' + get 'help/api' => 'help#api' get 'help/web_hooks' => 'help#web_hooks' + get 'help/system_hooks' => 'help#system_hooks' # # Admin Area @@ -46,11 +48,13 @@ Gitlab::Application.routes.draw do end end resources :team_members, :only => [:edit, :update, :destroy] - get 'emails', :to => 'mailer#preview' get 'mailer/preview_note' get 'mailer/preview_user_new' get 'mailer/preview_issue_new' + resources :hooks, :only => [:index, :create, :destroy] do + get :test + end resource :logs resource :resque, :controller => 'resque' root :to => "dashboard#index" @@ -116,6 +120,8 @@ Gitlab::Application.routes.draw do member do get "tree", :constraints => { :id => /[a-zA-Z.\/0-9_\-]+/ } + get "logs_tree", :constraints => { :id => /[a-zA-Z.\/0-9_\-]+/ } + get "blob", :constraints => { :id => /[a-zA-Z.0-9\/_\-]+/, @@ -131,6 +137,14 @@ Gitlab::Application.routes.draw do :path => /.*/ } + # tree viewer + get "logs_tree/:path" => "refs#logs_tree", + :as => :logs_file, + :constraints => { + :id => /[a-zA-Z.0-9\/_\-]+/, + :path => /.*/ + } + # blame get "blame/:path" => "refs#blame", :as => :blame_file, diff --git a/db/migrate/20110913200833_devise_create_users.rb b/db/migrate/20110913200833_devise_create_users.rb index 01869a9e21c..e00f275c55d 100644 --- a/db/migrate/20110913200833_devise_create_users.rb +++ b/db/migrate/20110913200833_devise_create_users.rb @@ -1,15 +1,43 @@ class DeviseCreateUsers < ActiveRecord::Migration def self.up create_table(:users) do |t| - t.database_authenticatable :null => false - t.recoverable - t.rememberable - t.trackable - - # t.encryptable - # t.confirmable - # t.lockable :lock_strategy => :failed_attempts, :unlock_strategy => :both - # t.token_authenticatable + ## Database authenticatable + t.string :email, :null => false, :default => "" + t.string :encrypted_password, :null => false, :default => "" + + ## Recoverable + t.string :reset_password_token + t.datetime :reset_password_sent_at + + ## Rememberable + t.datetime :remember_created_at + + ## Trackable + t.integer :sign_in_count, :default => 0 + t.datetime :current_sign_in_at + t.datetime :last_sign_in_at + t.string :current_sign_in_ip + t.string :last_sign_in_ip + + ## Encryptable + # t.string :password_salt + + ## Confirmable + # t.string :confirmation_token + # t.datetime :confirmed_at + # t.datetime :confirmation_sent_at + # t.string :unconfirmed_email # Only if using reconfirmable + + ## Lockable + # t.integer :failed_attempts, :default => 0 # Only if lock strategy is :failed_attempts + # t.string :unlock_token # Only if unlock strategy is :email or :both + # t.datetime :locked_at + + # Token authenticatable + # t.string :authentication_token + + ## Invitable + # t.string :invitation_token t.timestamps end diff --git a/db/migrate/20120706065612_add_lockable_to_users.rb b/db/migrate/20120706065612_add_lockable_to_users.rb new file mode 100644 index 00000000000..cf86e660876 --- /dev/null +++ b/db/migrate/20120706065612_add_lockable_to_users.rb @@ -0,0 +1,6 @@ +class AddLockableToUsers < ActiveRecord::Migration + def change + add_column :users, :failed_attempts, :integer, :default => 0 + add_column :users, :locked_at, :datetime + end +end diff --git a/db/migrate/20120712080407_add_type_to_web_hook.rb b/db/migrate/20120712080407_add_type_to_web_hook.rb new file mode 100644 index 00000000000..18ab024c817 --- /dev/null +++ b/db/migrate/20120712080407_add_type_to_web_hook.rb @@ -0,0 +1,5 @@ +class AddTypeToWebHook < ActiveRecord::Migration + def change + add_column :web_hooks, :type, :string, :default => "ProjectHook" + end +end diff --git a/db/schema.rb b/db/schema.rb index f2bb16937f4..c4c54f562a3 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -11,7 +11,7 @@ # # It's strongly recommended to check this file into your version control system. -ActiveRecord::Schema.define(:version => 20120627145613) do +ActiveRecord::Schema.define(:version => 20120712080407) do create_table "events", :force => true do |t| t.string "target_type" @@ -169,6 +169,8 @@ ActiveRecord::Schema.define(:version => 20120627145613) do t.integer "theme_id", :default => 1, :null => false t.string "bio" t.boolean "blocked", :default => false, :null => false + t.integer "failed_attempts", :default => 0 + t.datetime "locked_at" end add_index "users", ["email"], :name => "index_users_on_email", :unique => true @@ -185,8 +187,9 @@ ActiveRecord::Schema.define(:version => 20120627145613) do create_table "web_hooks", :force => true do |t| t.string "url" t.integer "project_id" - t.datetime "created_at", :null => false - t.datetime "updated_at", :null => false + t.datetime "created_at", :null => false + t.datetime "updated_at", :null => false + t.string "type", :default => "ProjectHook" end create_table "wikis", :force => true do |t| diff --git a/doc/installation.md b/doc/installation.md index bf579b174c3..3dfedfe10ad 100644 --- a/doc/installation.md +++ b/doc/installation.md @@ -60,7 +60,7 @@ Also read the [Read this before you submit an issue](https://github.com/gitlabhq sudo apt-get update sudo apt-get upgrade - sudo apt-get install -y wget curl gcc checkinstall libxml2-dev libxslt-dev sqlite3 libsqlite3-dev libcurl4-openssl-dev libreadline-gplv2-dev libc6-dev libssl-dev libmysql++-dev make build-essential zlib1g-dev libicu-dev redis-server openssh-server git-core python-dev python-pip libyaml-dev sendmail + sudo apt-get install -y wget curl gcc checkinstall libxml2-dev libxslt-dev sqlite3 libsqlite3-dev libcurl4-openssl-dev libreadline6-dev libc6-dev libssl-dev libmysql++-dev make build-essential zlib1g-dev libicu-dev redis-server openssh-server git-core python-dev python-pip libyaml-dev sendmail # If you want to use MySQL: sudo apt-get install -y mysql-server mysql-client libmysqlclient-dev @@ -107,10 +107,10 @@ Get gitolite source code: Setup: - sudo -u git sh -c 'echo -e "PATH=\$PATH:/home/git/bin\nexport PATH" > /home/git/.profile' + sudo -u git sh -c 'echo -e "PATH=\$PATH:/home/git/bin\nexport PATH" >> /home/git/.profile' sudo -u git -H sh -c "PATH=/home/git/bin:$PATH; /home/git/gitolite/src/gl-system-install" sudo cp /home/gitlab/.ssh/id_rsa.pub /home/git/gitlab.pub - sudo chmod 777 /home/git/gitlab.pub + sudo chmod 0444 /home/git/gitlab.pub sudo -u git -H sed -i 's/0077/0007/g' /home/git/share/gitolite/conf/example.gitolite.rc sudo -u git -H sh -c "PATH=/home/git/bin:$PATH; gl-setup -q /home/git/gitlab.pub" @@ -139,6 +139,8 @@ Permissions: cd /home/gitlab sudo -H -u gitlab git clone -b stable git://github.com/gitlabhq/gitlabhq.git gitlab cd gitlab + + sudo -u gitlab mkdir tmp # Rename config files sudo -u gitlab cp config/gitlab.yml.example config/gitlab.yml @@ -216,15 +218,15 @@ Application can be started with next command: sudo -u gitlab cp config/unicorn.rb.orig config/unicorn.rb sudo -u gitlab bundle exec unicorn_rails -c config/unicorn.rb -E production -D -Edit /etc/nginx/nginx.conf. Add in **http** section: +Edit /etc/nginx/nginx.conf. In the *http* section add: upstream gitlab { server unix:/home/gitlab/gitlab/tmp/sockets/gitlab.socket; } server { - listen YOUR_SERVER_IP:80; - server_name gitlab.YOUR_DOMAIN.com; + listen YOUR_SERVER_IP:80; # e.g., listen 192.168.1.1:80; + server_name YOUR_SERVER_FQDN; # e.g., server_name source.example.com; root /home/gitlab/gitlab/public; # individual nginx logs for this gitlab vhost @@ -232,26 +234,26 @@ Edit /etc/nginx/nginx.conf. Add in **http** section: error_log /var/log/nginx/gitlab_error.log; location / { - # serve static files from defined root folder;. - # @gitlab is a named location for the upstream fallback, see below - try_files $uri $uri/index.html $uri.html @gitlab; + # serve static files from defined root folder;. + # @gitlab is a named location for the upstream fallback, see below + try_files $uri $uri/index.html $uri.html @gitlab; } # if a file, which is not found in the root folder is requested, # then the proxy pass the request to the upsteam (gitlab unicorn) location @gitlab { proxy_redirect off; + # you need to change this to "https", if you set "ssl" directive to "on" proxy_set_header X-FORWARDED_PROTO http; - proxy_set_header Host gitlab.YOUR_SUBDOMAIN.com:80; + proxy_set_header Host $http_host; proxy_set_header X-Real-IP $remote_addr; proxy_pass http://gitlab; } - } -gitlab.YOUR_DOMAIN.com - change to your domain. +Change **YOUR_SERVER_IP** and **YOUR_SERVER_FQDN** to the IP address and fully-qualified domain name of the host serving GitLab. Restart nginx: diff --git a/lib/gitlab/logger.rb b/lib/gitlab/logger.rb index e8d20ad90c2..aff13baf67b 100644 --- a/lib/gitlab/logger.rb +++ b/lib/gitlab/logger.rb @@ -1,14 +1,24 @@ module Gitlab - class Logger + class Logger < ::Logger def self.error(message) - @@logger ||= ::Logger.new(File.join(Rails.root, "log/githost.log")) - message = Time.now.to_s(:long) + " -> " + message - @@logger.error(message) + build.error(message) + end + + def self.info(message) + build.info(message) end def self.read_latest path = Rails.root.join("log/githost.log") - logs = `tail -n 50 #{path}`.split("\n") + logs = File.read(path).split("\n") end + + def self.build + new(File.join(Rails.root, "log/githost.log")) + end + + def format_message(severity, timestamp, progname, msg) + "#{timestamp.to_s(:long)} -> #{severity} -> #{msg}\n" + end end end diff --git a/lib/tasks/gitlab/backup.rake b/lib/tasks/gitlab/backup.rake new file mode 100644 index 00000000000..014483d4e8c --- /dev/null +++ b/lib/tasks/gitlab/backup.rake @@ -0,0 +1,190 @@ +require 'active_record/fixtures' + +namespace :gitlab do + namespace :app do + + # Create backup of gitlab system + desc "GITLAB | Create a backup of the gitlab system" + task :backup_create => :environment do + + Rake::Task["gitlab:app:db_dump"].invoke + Rake::Task["gitlab:app:repo_dump"].invoke + + Dir.chdir(Gitlab.config.backup_path) + + # saving additional informations + s = Hash.new + s["db_version"] = "#{ActiveRecord::Migrator.current_version}" + s["backup_created_at"] = "#{Time.now}" + s["gitlab_version"] = %x{git rev-parse HEAD}.gsub(/\n/,"") + s["tar_version"] = %x{tar --version | head -1}.gsub(/\n/,"") + + File.open("#{Gitlab.config.backup_path}/backup_information.yml", "w+") do |file| + file << s.to_yaml.gsub(/^---\n/,'') + end + + # create archive + print "Creating backup archive: #{Time.now.to_i}_gitlab_backup.tar " + if Kernel.system("tar -cf #{Time.now.to_i}_gitlab_backup.tar repositories/ db/ backup_information.yml") + puts "[DONE]".green + else + puts "[FAILED]".red + end + + # cleanup: remove tmp files + print "Deletion of tmp directories..." + if Kernel.system("rm -rf repositories/ db/ backup_information.yml") + puts "[DONE]".green + else + puts "[FAILED]".red + end + + # delete backups + print "Deleting old backups... " + if Gitlab.config.backup_keep_time > 0 + file_list = Dir.glob("*_gitlab_backup.tar").map { |f| f.split(/_/).first.to_i } + file_list.sort.each do |timestamp| + if Time.at(timestamp) < (Time.now - Gitlab.config.backup_keep_time) + %x{rm #{timestamp}_gitlab_backup.tar} + end + end + puts "[DONE]".green + else + puts "[SKIPPING]".yellow + end + + end + + + # Restore backup of gitlab system + desc "GITLAB | Restore a previously created backup" + task :backup_restore => :environment do + + Dir.chdir(Gitlab.config.backup_path) + + # check for existing backups in the backup dir + file_list = Dir.glob("*_gitlab_backup.tar").each.map { |f| f.split(/_/).first.to_i } + puts "no backup found" if file_list.count == 0 + if file_list.count > 1 && ENV["BACKUP"].nil? + puts "Found more than one backup, please specify which one you want to restore:" + puts "rake gitlab:app:backup_restore BACKUP=timestamp_of_backup" + exit 1; + end + + tar_file = ENV["BACKUP"].nil? ? File.join(file_list.first.to_s + "_gitlab_backup.tar") : File.join(ENV["BACKUP"] + "_gitlab_backup.tar") + + unless File.exists?(tar_file) + puts "The specified backup doesn't exist!" + exit 1; + end + + print "Unpacking backup... " + unless Kernel.system("tar -xf #{tar_file}") + puts "[FAILED]".red + exit 1 + else + puts "[DONE]".green + end + + 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 + end + + Rake::Task["gitlab:app:db_restore"].invoke + Rake::Task["gitlab:app:repo_restore"].invoke + + # cleanup: remove tmp files + print "Deletion of tmp directories..." + if Kernel.system("rm -rf repositories/ db/ backup_information.yml") + puts "[DONE]".green + else + puts "[FAILED]".red + end + + end + + + ################################################################################ + ################################# invoked tasks ################################ + + ################################# REPOSITORIES ################################# + + task :repo_dump => :environment do + backup_path_repo = File.join(Gitlab.config.backup_path, "repositories") + FileUtils.mkdir_p(backup_path_repo) until Dir.exists?(backup_path_repo) + puts "Dumping repositories:" + project = Project.all.map { |n| [n.name,n.path_to_repo] } + project << ["gitolite-admin.git", File.join(File.dirname(project.first.second), "gitolite-admin.git")] + project.each do |project| + print "- Dumping repository #{project.first}... " + if Kernel.system("cd #{project.second} > /dev/null 2>&1 && git bundle create #{backup_path_repo}/#{project.first}.bundle --all > /dev/null 2>&1") + puts "[DONE]".green + else + puts "[FAILED]".red + end + end + end + + task :repo_restore => :environment do + backup_path_repo = File.join(Gitlab.config.backup_path, "repositories") + puts "Restoring repositories:" + project = Project.all.map { |n| [n.name,n.path_to_repo] } + project << ["gitolite-admin.git", File.join(File.dirname(project.first.second), "gitolite-admin.git")] + project.each do |project| + print "- Restoring repository #{project.first}... " + FileUtils.rm_rf(project.second) if File.dirname(project.second) # delet old stuff + if Kernel.system("cd #{File.dirname(project.second)} > /dev/null 2>&1 && git clone --bare #{backup_path_repo}/#{project.first}.bundle #{project.first}.git > /dev/null 2>&1") + puts "[DONE]".green + else + puts "[FAILED]".red + end + end + end + + ###################################### DB ###################################### + + task :db_dump => :environment do + backup_path_db = File.join(Gitlab.config.backup_path, "db") + FileUtils.mkdir_p(backup_path_db) until Dir.exists?(backup_path_db) + puts "Dumping database tables:" + ActiveRecord::Base.connection.tables.each do |tbl| + print "- Dumping table #{tbl}... " + count = 1 + File.open(File.join(backup_path_db, tbl + ".yml"), "w+") do |file| + ActiveRecord::Base.connection.select_all("SELECT * FROM `#{tbl}`").each do |line| + line.delete_if{|k,v| v.blank?} + output = {tbl + '_' + count.to_s => line} + file << output.to_yaml.gsub(/^---\n/,'') + "\n" + count += 1 + end + puts "[DONE]".green + end + end + end + + task :db_restore=> :environment do + backup_path_db = File.join(Gitlab.config.backup_path, "db") + puts "Restoring database tables:" + Rake::Task["db:reset"].invoke + Dir.glob(File.join(backup_path_db, "*.yml") ).each do |dir| + fixture_file = File.basename(dir, ".*" ) + print "- Loading fixture #{fixture_file}..." + if File.size(dir) > 0 + ActiveRecord::Fixtures.create_fixtures(backup_path_db, fixture_file) + puts "[DONE]".green + else + puts "[SKIPPING]".yellow + end + end + end + + end # namespace end: app +end # namespace end: gitlab diff --git a/resque.sh b/resque.sh index ce7c944b735..ab67c650805 100755 --- a/resque.sh +++ b/resque.sh @@ -1,2 +1,2 @@ mkdir -p tmp/pids -bundle exec rake environment resque:work QUEUE=post_receive,mailer RAILS_ENV=production PIDFILE=tmp/pids/resque_worker.pid BACKGROUND=yes +bundle exec rake environment resque:work QUEUE=post_receive,mailer,system_hook RAILS_ENV=production PIDFILE=tmp/pids/resque_worker.pid BACKGROUND=yes diff --git a/resque_dev.sh b/resque_dev.sh index 9df4dc1d087..b09cfd9e383 100755 --- a/resque_dev.sh +++ b/resque_dev.sh @@ -1 +1 @@ -bundle exec rake environment resque:work QUEUE=* VVERBOSE=1 +bundle exec rake environment resque:work QUEUE=post_receive,mailer,system_hook VVERBOSE=1 diff --git a/spec/api/projects_spec.rb b/spec/api/projects_spec.rb index a4e875f73c6..9998ee509bf 100644 --- a/spec/api/projects_spec.rb +++ b/spec/api/projects_spec.rb @@ -78,7 +78,7 @@ describe Gitlab::API do end describe "DELETE /projects/:id/snippets/:snippet_id" do - it "should create a new project snippet" do + it "should delete existing project snippet" do expect { delete "#{api_prefix}/projects/#{project.code}/snippets/#{snippet.id}?private_token=#{user.private_token}" }.should change { Snippet.count }.by(-1) diff --git a/spec/factories.rb b/spec/factories.rb index ea8c7aef0e2..ab2ca4687da 100644 --- a/spec/factories.rb +++ b/spec/factories.rb @@ -7,6 +7,12 @@ Factory.add(:project, Project) do |obj| obj.code = 'LGT' end +Factory.add(:project_without_owner, Project) do |obj| + obj.name = Faker::Internet.user_name + obj.path = 'gitlabhq' + obj.code = 'LGT' +end + Factory.add(:public_project, Project) do |obj| obj.name = Faker::Internet.user_name obj.path = 'gitlabhq' @@ -60,7 +66,11 @@ Factory.add(:key, Key) do |obj| obj.key = File.read(File.join(Rails.root, "db", "pkey.example")) end -Factory.add(:web_hook, WebHook) do |obj| +Factory.add(:project_hook, ProjectHook) do |obj| + obj.url = Faker::Internet.uri("http") +end + +Factory.add(:system_hook, SystemHook) do |obj| obj.url = Faker::Internet.uri("http") end diff --git a/spec/models/merge_request_spec.rb b/spec/models/merge_request_spec.rb index 8d750bef5a5..ac986ccebe3 100644 --- a/spec/models/merge_request_spec.rb +++ b/spec/models/merge_request_spec.rb @@ -13,7 +13,6 @@ describe MergeRequest do it { should validate_presence_of(:title) } it { should validate_presence_of(:author_id) } it { should validate_presence_of(:project_id) } - it { should validate_presence_of(:assignee_id) } end describe "Scope" do diff --git a/spec/models/project_hooks_spec.rb b/spec/models/project_hooks_spec.rb index fcc969ceba5..5544c5a8683 100644 --- a/spec/models/project_hooks_spec.rb +++ b/spec/models/project_hooks_spec.rb @@ -21,44 +21,44 @@ describe Project, "Hooks" do end end - describe "Web hooks" do + describe "Project hooks" do context "with no web hooks" do it "raises no errors" do lambda { - project.execute_web_hooks('oldrev', 'newrev', 'ref', @user) + project.execute_hooks('oldrev', 'newrev', 'ref', @user) }.should_not raise_error end end context "with web hooks" do before do - @webhook = Factory(:web_hook) - @webhook_2 = Factory(:web_hook) - project.web_hooks << [@webhook, @webhook_2] + @project_hook = Factory(:project_hook) + @project_hook_2 = Factory(:project_hook) + project.hooks << [@project_hook, @project_hook_2] end it "executes multiple web hook" do - @webhook.should_receive(:execute).once - @webhook_2.should_receive(:execute).once + @project_hook.should_receive(:execute).once + @project_hook_2.should_receive(:execute).once - project.execute_web_hooks('oldrev', 'newrev', 'refs/heads/master', @user) + project.execute_hooks('oldrev', 'newrev', 'refs/heads/master', @user) end end context "does not execute web hooks" do before do - @webhook = Factory(:web_hook) - project.web_hooks << [@webhook] + @project_hook = Factory(:project_hook) + project.hooks << [@project_hook] end it "when pushing a branch for the first time" do - @webhook.should_not_receive(:execute) - project.execute_web_hooks('00000000000000000000000000000000', 'newrev', 'refs/heads/master', @user) + @project_hook.should_not_receive(:execute) + project.execute_hooks('00000000000000000000000000000000', 'newrev', 'refs/heads/master', @user) end it "when pushing tags" do - @webhook.should_not_receive(:execute) - project.execute_web_hooks('oldrev', 'newrev', 'refs/tags/v1.0.0', @user) + @project_hook.should_not_receive(:execute) + project.execute_hooks('oldrev', 'newrev', 'refs/tags/v1.0.0', @user) end end diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb index 381fe7592c9..351f5748b4d 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -11,7 +11,7 @@ describe Project do it { should have_many(:issues).dependent(:destroy) } it { should have_many(:notes).dependent(:destroy) } it { should have_many(:snippets).dependent(:destroy) } - it { should have_many(:web_hooks).dependent(:destroy) } + it { should have_many(:hooks).dependent(:destroy) } it { should have_many(:deploy_keys).dependent(:destroy) } end diff --git a/spec/models/system_hook_spec.rb b/spec/models/system_hook_spec.rb new file mode 100644 index 00000000000..4ad4d1681fc --- /dev/null +++ b/spec/models/system_hook_spec.rb @@ -0,0 +1,63 @@ +require "spec_helper" + +describe SystemHook do + describe "execute" do + before(:each) { ActiveRecord::Base.observers.enable(:all) } + + before(:each) do + @system_hook = Factory :system_hook + WebMock.stub_request(:post, @system_hook.url) + end + + it "project_create hook" do + user = Factory :user + with_resque do + project = Factory :project_without_owner, :owner => user + end + WebMock.should have_requested(:post, @system_hook.url).with(body: /project_create/).once + end + + it "project_destroy hook" do + project = Factory :project + with_resque do + project.destroy + end + WebMock.should have_requested(:post, @system_hook.url).with(body: /project_destroy/).once + end + + it "user_create hook" do + with_resque do + Factory :user + end + WebMock.should have_requested(:post, @system_hook.url).with(body: /user_create/).once + end + + it "user_destroy hook" do + user = Factory :user + with_resque do + user.destroy + end + WebMock.should have_requested(:post, @system_hook.url).with(body: /user_destroy/).once + end + + it "project_create hook" do + user = Factory :user + project = Factory :project + with_resque do + project.users << user + end + WebMock.should have_requested(:post, @system_hook.url).with(body: /user_add_to_team/).once + end + + it "project_destroy hook" do + user = Factory :user + project = Factory :project + project.users << user + with_resque do + project.users_projects.clear + end + WebMock.should have_requested(:post, @system_hook.url).with(body: /user_remove_from_team/).once + end + end + +end diff --git a/spec/models/web_hook_spec.rb b/spec/models/web_hook_spec.rb index 9971bd5819d..885947614d7 100644 --- a/spec/models/web_hook_spec.rb +++ b/spec/models/web_hook_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe WebHook do +describe ProjectHook do describe "Associations" do it { should belong_to :project } end @@ -23,32 +23,32 @@ describe WebHook do describe "execute" do before(:each) do - @webhook = Factory :web_hook + @project_hook = Factory :project_hook @project = Factory :project - @project.web_hooks << [@webhook] + @project.hooks << [@project_hook] @data = { before: 'oldrev', after: 'newrev', ref: 'ref'} - WebMock.stub_request(:post, @webhook.url) + WebMock.stub_request(:post, @project_hook.url) end it "POSTs to the web hook URL" do - @webhook.execute(@data) - WebMock.should have_requested(:post, @webhook.url).once + @project_hook.execute(@data) + WebMock.should have_requested(:post, @project_hook.url).once end it "POSTs the data as JSON" do json = @data.to_json - @webhook.execute(@data) - WebMock.should have_requested(:post, @webhook.url).with(body: json).once + @project_hook.execute(@data) + WebMock.should have_requested(:post, @project_hook.url).with(body: json).once end it "catches exceptions" do WebHook.should_receive(:post).and_raise("Some HTTP Post error") lambda { - @webhook.execute(@data) - }.should_not raise_error + @project_hook.execute(@data) + }.should raise_error end end end diff --git a/spec/requests/admin/admin_hooks_spec.rb b/spec/requests/admin/admin_hooks_spec.rb new file mode 100644 index 00000000000..e8a345b689f --- /dev/null +++ b/spec/requests/admin/admin_hooks_spec.rb @@ -0,0 +1,53 @@ +require 'spec_helper' + +describe "Admin::Hooks" do + before do + @project = Factory :project, + :name => "LeGiT", + :code => "LGT" + login_as :admin + + @system_hook = Factory :system_hook + + end + + describe "GET /admin/hooks" do + it "should be ok" do + visit admin_root_path + within ".main_menu" do + click_on "Hooks" + end + current_path.should == admin_hooks_path + end + + it "should have hooks list" do + visit admin_hooks_path + page.should have_content(@system_hook.url) + end + end + + describe "New Hook" do + before do + @url = Faker::Internet.uri("http") + visit admin_hooks_path + fill_in "hook_url", :with => @url + expect { click_button "Add System Hook" }.to change(SystemHook, :count).by(1) + end + + it "should open new hook popup" do + page.current_path.should == admin_hooks_path + page.should have_content(@url) + end + end + + describe "Test" do + before do + WebMock.stub_request(:post, @system_hook.url) + visit admin_hooks_path + click_link "Test Hook" + end + + it { page.current_path.should == admin_hooks_path } + end + +end diff --git a/spec/requests/admin/security_spec.rb b/spec/requests/admin/security_spec.rb index 0b0edb85a37..0c369740cff 100644 --- a/spec/requests/admin/security_spec.rb +++ b/spec/requests/admin/security_spec.rb @@ -13,9 +13,9 @@ describe "Admin::Projects" do it { admin_users_path.should be_denied_for :visitor } end - describe "GET /admin/emails" do - it { admin_emails_path.should be_allowed_for :admin } - it { admin_emails_path.should be_denied_for :user } - it { admin_emails_path.should be_denied_for :visitor } + describe "GET /admin/hooks" do + it { admin_hooks_path.should be_allowed_for :admin } + it { admin_hooks_path.should be_denied_for :user } + it { admin_hooks_path.should be_denied_for :visitor } end end diff --git a/spec/requests/hooks_spec.rb b/spec/requests/hooks_spec.rb index a508e5ea517..05432f13f3c 100644 --- a/spec/requests/hooks_spec.rb +++ b/spec/requests/hooks_spec.rb @@ -9,7 +9,7 @@ describe "Hooks" do describe "GET index" do it "should be available" do - @hook = Factory :web_hook, :project => @project + @hook = Factory :project_hook, :project => @project visit project_hooks_path(@project) page.should have_content "Hooks" page.should have_content @hook.url @@ -21,7 +21,7 @@ describe "Hooks" do @url = Faker::Internet.uri("http") visit project_hooks_path(@project) fill_in "hook_url", :with => @url - expect { click_button "Add Web Hook" }.to change(WebHook, :count).by(1) + expect { click_button "Add Web Hook" }.to change(ProjectHook, :count).by(1) end it "should open new team member popup" do @@ -32,7 +32,8 @@ describe "Hooks" do describe "Test" do before do - @hook = Factory :web_hook, :project => @project + @hook = Factory :project_hook, :project => @project + stub_request(:post, @hook.url) visit project_hooks_path(@project) click_link "Test Hook" end diff --git a/spec/workers/post_receive_spec.rb b/spec/workers/post_receive_spec.rb index c70b2f6cebb..5f3b6d3daec 100644 --- a/spec/workers/post_receive_spec.rb +++ b/spec/workers/post_receive_spec.rb @@ -22,14 +22,14 @@ describe PostReceive do Key.stub(find_by_identifier: nil) project.should_not_receive(:observe_push) - project.should_not_receive(:execute_web_hooks) + project.should_not_receive(:execute_hooks) PostReceive.perform(project.path, 'sha-old', 'sha-new', 'refs/heads/master', key_id).should be_false end it "asks the project to execute web hooks" do Project.stub(find_by_path: project) - project.should_receive(:execute_web_hooks).with('sha-old', 'sha-new', 'refs/heads/master', project.owner) + project.should_receive(:execute_hooks).with('sha-old', 'sha-new', 'refs/heads/master', project.owner) PostReceive.perform(project.path, 'sha-old', 'sha-new', 'refs/heads/master', key_id) end diff --git a/vendor/assets/javascripts/jquery.waitforimages.js b/vendor/assets/javascripts/jquery.waitforimages.js new file mode 100644 index 00000000000..95b39c2e074 --- /dev/null +++ b/vendor/assets/javascripts/jquery.waitforimages.js @@ -0,0 +1,144 @@ +/* + * waitForImages 1.4 + * ----------------- + * Provides a callback when all images have loaded in your given selector. + * http://www.alexanderdickson.com/ + * + * + * Copyright (c) 2011 Alex Dickson + * Licensed under the MIT licenses. + * See website for more info. + * + */ + +;(function($) { + // Namespace all events. + var eventNamespace = 'waitForImages'; + + // CSS properties which contain references to images. + $.waitForImages = { + hasImageProperties: [ + 'backgroundImage', + 'listStyleImage', + 'borderImage', + 'borderCornerImage' + ] + }; + + // Custom selector to find `img` elements that have a valid `src` attribute and have not already loaded. + $.expr[':'].uncached = function(obj) { + // Ensure we are dealing with an `img` element with a valid `src` attribute. + if ( ! $(obj).is('img[src!=""]')) { + return false; + } + + // Firefox's `complete` property will always be`true` even if the image has not been downloaded. + // Doing it this way works in Firefox. + var img = document.createElement('img'); + img.src = obj.src; + return ! img.complete; + }; + + $.fn.waitForImages = function(finishedCallback, eachCallback, waitForAll) { + + // Handle options object. + if ($.isPlainObject(arguments[0])) { + eachCallback = finishedCallback.each; + waitForAll = finishedCallback.waitForAll; + finishedCallback = finishedCallback.finished; + } + + // Handle missing callbacks. + finishedCallback = finishedCallback || $.noop; + eachCallback = eachCallback || $.noop; + + // Convert waitForAll to Boolean + waitForAll = !! waitForAll; + + // Ensure callbacks are functions. + if (!$.isFunction(finishedCallback) || !$.isFunction(eachCallback)) { + throw new TypeError('An invalid callback was supplied.'); + }; + + return this.each(function() { + // Build a list of all imgs, dependent on what images will be considered. + var obj = $(this), + allImgs = []; + + if (waitForAll) { + // CSS properties which may contain an image. + var hasImgProperties = $.waitForImages.hasImageProperties || [], + matchUrl = /url\((['"]?)(.*?)\1\)/g; + + // Get all elements, as any one of them could have a background image. + obj.find('*').each(function() { + var element = $(this); + + // If an `img` element, add it. But keep iterating in case it has a background image too. + if (element.is('img:uncached')) { + allImgs.push({ + src: element.attr('src'), + element: element[0] + }); + } + + $.each(hasImgProperties, function(i, property) { + var propertyValue = element.css(property); + // If it doesn't contain this property, skip. + if ( ! propertyValue) { + return true; + } + + // Get all url() of this element. + var match; + while (match = matchUrl.exec(propertyValue)) { + allImgs.push({ + src: match[2], + element: element[0] + }); + }; + }); + }); + } else { + // For images only, the task is simpler. + obj + .find('img:uncached') + .each(function() { + allImgs.push({ + src: this.src, + element: this + }); + }); + }; + + var allImgsLength = allImgs.length, + allImgsLoaded = 0; + + // If no images found, don't bother. + if (allImgsLength == 0) { + finishedCallback.call(obj[0]); + }; + + $.each(allImgs, function(i, img) { + + var image = new Image; + + // Handle the image loading and error with the same callback. + $(image).bind('load.' + eventNamespace + ' error.' + eventNamespace, function(event) { + allImgsLoaded++; + + // If an error occurred with loading the image, set the third argument accordingly. + eachCallback.call(img.element, allImgsLoaded, allImgsLength, event.type == 'load'); + + if (allImgsLoaded == allImgsLength) { + finishedCallback.call(obj[0]); + return false; + }; + + }); + + image.src = img.src; + }); + }); + }; +})(jQuery); |