diff options
110 files changed, 2502 insertions, 2444 deletions
diff --git a/CHANGELOG b/CHANGELOG index 6868c07ba52..7322efbf228 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,17 @@ +v 2.9.0 + - fixed inline notes bugs + - refactored rspecs + - refactored gitolite backend + - added factory_girl + - restyled projects list on dashboard + - ssh keys validation to prevent gitolite crash + - send notifications if changed premission in project + - scss refactoring. gitlab_bootstrap/ dir + - fix git push http body bigger than 112k problem + - list of labels page under issues tab + - API for milestones + - restyled buttons + v 2.8.1 - ability to disable gravatars - improved MR diff logic @@ -120,5 +120,5 @@ group :test do end group :production do - gem "gitlab_meta", '2.8' + gem "gitlab_meta", '2.9' end diff --git a/Gemfile.lock b/Gemfile.lock index b8e97aa0204..f226c931bff 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -178,7 +178,7 @@ GEM gherkin (2.11.0) json (>= 1.4.6) git (1.2.5) - gitlab_meta (2.8) + gitlab_meta (2.9) grape (0.2.1) hashie (~> 1.2) multi_json @@ -397,7 +397,7 @@ DEPENDENCIES ffaker foreman git - gitlab_meta (= 2.8) + gitlab_meta (= 2.9) gitolite! grack! grape (~> 0.2.1) diff --git a/README.md b/README.md index 26ed209ef87..122cd984b4a 100644 --- a/README.md +++ b/README.md @@ -39,5 +39,6 @@ Email ## Contribute +[Development Tips](https://github.com/gitlabhq/gitlabhq/blob/master/doc/development.md) Want to help - send a pull request. We'll accept good pull requests. @@ -1 +1 @@ -2.8.2 +2.9.0pre diff --git a/app/assets/images/merge.png b/app/assets/images/merge.png Binary files differnew file mode 100644 index 00000000000..4a6bb2e154d --- /dev/null +++ b/app/assets/images/merge.png diff --git a/app/assets/javascripts/application.js b/app/assets/javascripts/application.js index 24d99a62ca5..f69fd6f9a44 100644 --- a/app/assets/javascripts/application.js +++ b/app/assets/javascripts/application.js @@ -128,3 +128,23 @@ function showDiff(link) { function ajaxGet(url) { $.ajax({type: "GET", url: url, dataType: "script"}); } + +/** + * Disable button if text field is empty + */ +function disableButtonIfEmtpyField(field_selector, button_selector) { + field = $(field_selector); + if(field.val() == "") { + field.closest("form").find(button_selector).attr("disabled", "disabled").addClass("disabled"); + } + + field.on('keyup', function(){ + var field = $(this); + var closest_submit = field.closest("form").find(button_selector); + if(field.val() == "") { + closest_submit.attr("disabled", "disabled").addClass("disabled"); + } else { + closest_submit.removeAttr("disabled").removeClass("disabled"); + } + }) +} diff --git a/app/assets/javascripts/issues.js b/app/assets/javascripts/issues.js index bc0569654e1..aae818deefc 100644 --- a/app/assets/javascripts/issues.js +++ b/app/assets/javascripts/issues.js @@ -5,6 +5,7 @@ function switchToNewIssue(form){ $('select#issue_milestone_id').chosen(); $("#new_issue_dialog").show("fade", { direction: "right" }, 150); $('.top-tabs .add_new').hide(); + disableButtonIfEmtpyField("#issue_title", ".save-btn"); }); } @@ -15,6 +16,7 @@ function switchToEditIssue(form){ $('select#issue_milestone_id').chosen(); $("#edit_issue_dialog").show("fade", { direction: "right" }, 150); $('.add_new').hide(); + disableButtonIfEmtpyField("#issue_title", ".save-btn"); }); } diff --git a/app/assets/javascripts/note.js b/app/assets/javascripts/note.js index d9ae45d93c7..9cd3e36e87b 100644 --- a/app/assets/javascripts/note.js +++ b/app/assets/javascripts/note.js @@ -1,161 +1,160 @@ var NoteList = { -notes_path: null, -target_params: null, -target_id: 0, -target_type: null, -first_id: 0, -last_id: 0, -disable:false, - -init: - function(tid, tt, path) { - this.notes_path = path + ".js"; - this.target_id = tid; - this.target_type = tt; - this.target_params = "&target_type=" + this.target_type + "&target_id=" + this.target_id; - - // get notes - this.getContent(); - - // get new notes every n seconds - this.initRefresh(); - - $('.delete-note').live('ajax:success', function() { - $(this).closest('li').fadeOut(); }); - - $("#new_note").live("ajax:before", function(){ - $(".submit_note").attr("disabled", "disabled"); - }) - - $("#new_note").live("ajax:complete", function(){ - $(".submit_note").removeAttr("disabled"); - }) - - $("#note_note").live("focus", function(){ - $(this).css("height", "80px"); - $('.note_advanced_opts').show(); - }); - - $("#note_attachment").change(function(e){ + notes_path: null, + target_params: null, + target_id: 0, + target_type: null, + first_id: 0, + last_id: 0, + disable:false, + + init: + function(tid, tt, path) { + this.notes_path = path + ".js"; + this.target_id = tid; + this.target_type = tt; + this.target_params = "&target_type=" + this.target_type + "&target_id=" + this.target_id; + + // get notes + this.getContent(); + + // get new notes every n seconds + this.initRefresh(); + + $('.delete-note').live('ajax:success', function() { + $(this).closest('li').fadeOut(); }); + + $(".note-form-holder").live("ajax:before", function(){ + $(".submit_note").attr("disabled", "disabled"); + }) + + $(".note-form-holder").live("ajax:complete", function(){ + $(".submit_note").removeAttr("disabled"); + }) + + disableButtonIfEmtpyField(".note-text", ".submit_note"); + + $(".note-text").live("focus", function(){ + $(this).css("height", "80px"); + $('.note_advanced_opts').show(); + }); + + $("#note_attachment").change(function(e){ var val = $('.input-file').val(); var filename = val.replace(/^.*[\\\/]/, ''); $(".file_name").text(filename); - }); + }); - }, + }, -/** - * Load new notes to fresh list called 'new_notes_list': - * - Replace 'new_notes_list' with new list every n seconds - * - Append new notes to this list after submit - */ + /** + * Load new notes to fresh list called 'new_notes_list': + * - Replace 'new_notes_list' with new list every n seconds + * - Append new notes to this list after submit + */ -initRefresh: - function() { - // init timer - var intNew = setInterval("NoteList.getNew()", 10000); - }, + initRefresh: + function() { + // init timer + var intNew = setInterval("NoteList.getNew()", 10000); + }, -replace: - function(html) { - $("#new_notes_list").html(html); - }, + replace: + function(html) { + $("#new_notes_list").html(html); + }, -prepend: - function(id, html) { - if(id != this.last_id) { - $("#new_notes_list").prepend(html); - } - }, + prepend: + function(id, html) { + if(id != this.last_id) { + $("#new_notes_list").prepend(html); + } + }, -getNew: - function() { - // refersh notes list - $.ajax({ - type: "GET", + getNew: + function() { + // refersh notes list + $.ajax({ + type: "GET", url: this.notes_path, data: "last_id=" + this.last_id + this.target_params, dataType: "script"}); - }, + }, -refresh: - function() { - // refersh notes list - $.ajax({ - type: "GET", + refresh: + function() { + // refersh notes list + $.ajax({ + type: "GET", url: this.notes_path, data: "first_id=" + this.first_id + "&last_id=" + this.last_id + this.target_params, dataType: "script"}); - }, + }, -/** - * Init load of notes: - * 1. Get content with ajax call - * 2. Set content of notes list with loaded one - */ + /** + * Init load of notes: + * 1. Get content with ajax call + * 2. Set content of notes list with loaded one + */ -getContent: - function() { - $.ajax({ - type: "GET", + getContent: + function() { + $.ajax({ + type: "GET", url: this.notes_path, data: "?" + this.target_params, complete: function(){ $('.status').removeClass("loading")}, beforeSend: function() { $('.status').addClass("loading") }, dataType: "script"}); - }, + }, -setContent: - function(fid, lid, html) { + setContent: + function(fid, lid, html) { this.last_id = lid; this.first_id = fid; $("#notes-list").html(html); // Init infinite scrolling this.initLoadMore(); - }, - - -/** - * Paging for old notes when scroll to bottom: - * 1. Init scroll events with 'initLoadMore' - * 2. Load onlder notes with 'getOld' method - * 3. append old notes to bottom of list with 'append' - * - */ - - -getOld: - function() { - $('.loading').show(); - $.ajax({ - type: "GET", - url: this.notes_path, - data: "first_id=" + this.first_id + this.target_params, - complete: function(){ $('.status').removeClass("loading")}, - beforeSend: function() { $('.status').addClass("loading") }, - dataType: "script"}); - }, - -append: - function(id, html) { - if(this.first_id == id) { - this.disable = true; - } else { - this.first_id = id; - $("#notes-list").append(html); - } - }, - + }, + + + /** + * Paging for old notes when scroll to bottom: + * 1. Init scroll events with 'initLoadMore' + * 2. Load onlder notes with 'getOld' method + * 3. append old notes to bottom of list with 'append' + * + */ + getOld: + function() { + $('.loading').show(); + $.ajax({ + type: "GET", + url: this.notes_path, + data: "first_id=" + this.first_id + this.target_params, + complete: function(){ $('.status').removeClass("loading")}, + beforeSend: function() { $('.status').addClass("loading") }, + dataType: "script"}); + }, + + append: + function(id, html) { + if(this.first_id == id) { + this.disable = true; + } else { + this.first_id = id; + $("#notes-list").append(html); + } + }, -initLoadMore: - function() { - $(document).endlessScroll({ - bottomPixels: 400, + initLoadMore: + function() { + $(document).endlessScroll({ + bottomPixels: 400, fireDelay: 1000, fireOnce:true, ceaseFire: function() { @@ -164,6 +163,20 @@ initLoadMore: callback: function(i) { NoteList.getOld(); } - }); - } + }); + } +}; + +var PerLineNotes = { + init: + function() { + $(".line_note_link, .line_note_reply_link").live("click", function(e) { + var form = $(".per_line_form"); + $(this).closest("tr").after(form); + form.find("#note_line_code").val($(this).attr("line_code")); + form.show(); + return false; + }); + disableButtonIfEmtpyField(".line-note-text", ".submit_inline_note"); + } } diff --git a/app/assets/javascripts/projects.js b/app/assets/javascripts/projects.js index 842726981d3..be1b75b90a7 100644 --- a/app/assets/javascripts/projects.js +++ b/app/assets/javascripts/projects.js @@ -7,8 +7,10 @@ function Projects() { $('.new_project, .edit_project').live('ajax:before', function() { $('.project_new_holder, .project_edit_holder').hide(); - $('.ajax_loader').show(); + $('.save-project-loader').show(); }); $('form #project_default_branch').chosen(); + + disableButtonIfEmtpyField("#project_name", ".project-submit") } diff --git a/app/assets/stylesheets/common.scss b/app/assets/stylesheets/common.scss index 68f862b8d2a..aa27a280a18 100644 --- a/app/assets/stylesheets/common.scss +++ b/app/assets/stylesheets/common.scss @@ -1,11 +1,9 @@ -.diff_file_header a, -.file_stats a { - color:$style_color; -} - - /** LAYOUT **/ +body { + margin-bottom:20px; +} + .container { padding-top:0; z-index:5; @@ -40,30 +38,6 @@ color: $link_color; } -.widget { - @include shade; - padding:20px; - margin-bottom:20px; - border: 1px solid #DDD; - border-radius: 5px; - background:#fafafa; - - .link_holder { - background:#eee; - position:relative; - left:-20px; - top:20px; - padding:10px 20px; - width:100%; - border-top:1px solid #ccc; - - a { - font-size:14px; - color:#666; - } - } -} - .help li { color:#111 } .back_link { @@ -88,16 +62,6 @@ padding-left:20px; } -.number { - border-radius: 4px; - text-shadow: none; - background: rgba(0,0,0,.12); - text-align: center; - padding: 2px 4px; - line-height:18px; - margin-left:2px; -} - table a code { position: relative; top: -2px; @@ -141,16 +105,6 @@ table a code { .git_url_wrapper { margin-right:50px } -.file_stats { - span { - img { - width:14px; - float:left; - margin-right:6px; - padding:2px 0; - } - } -} .handle:hover { cursor:move; @@ -174,10 +128,6 @@ span.update-author { display:block; } /** END UPDATE ITEM **/ -.ajax-tab-loading { - padding:40px; - display:none; -} .dashboard-loader { float:left; margin:10px; @@ -188,15 +138,110 @@ span.update-author { font-weight:bold; } -a.project-update.titled { - position:relative; - padding-left:35% !important; - .title-block { - padding:10px; - width:35%; - position:absolute; - left:0; - top:0; +.neib { + margin-right:10px; +} + +.label { + background-color: #474D57; + + &.label-issue { + background-color: #eee; + border: 1px solid #ccc; + padding:4px 6px; + color:#444; + text-shadow:0 0 1px #fff; + + &.grouped { + float: left; + margin-right: 6px; + padding: 6px; + } + } +} + +.event_label { + @extend .label; + background-color: #999; + + &.pushed { + background-color: #4A97BD; + } + + &.opened { + background-color: #469847; + } + + &.closed { + background-color: #B94A48; + } + + &.merged { + background-color: #2A2; + } +} + +form { + @extend .form-horizontal; + + .actions { + @extend .form-actions; + } + + .clearfix { + @extend .control-group; + } + + .input { + @extend .controls; + } + + label { + @extend .control-label; + } + .xlarge { + @extend .input-xlarge; + } + .xxlarge { + @extend .input-xxlarge; + } +} + + +.field_with_errors { + display:inline; +} + +ul.breadcrumb { + background:white; + border:none; + li { + display: inline; + text-shadow: 0 1px 0 white + } + + a { + color:#474D57; + font-weight:bold; + font-size:14px; + } + + .arrow { + background: url("images.png") no-repeat -85px -77px; + width: 19px; + height: 16px; + float: left; + position: relative; + left: -10px; + padding:0; + margin:0; + } +} + +input[type=text] { + &.large_text { + padding:6px; + font-size:16px; } } @@ -272,40 +317,6 @@ p.time { } -/** - * Dashboard page - * - */ -.dashboard_category { - margin-bottom:30px; - h3 a { - color:#474D57; - &:hover { - text-decoration:underline; - } - } - - .dashboard_block { - .dash_project_item { - margin-bottom:10px; - border:none; - padding:0px 5px; - .project_link { - color:#888; - &:hover { - color:#111; - .ico.project { - background-position:-209px -21px; - } - } - } - h4 { - color:#666; - } - } - } -} - .styled_image { border:2px solid #ddd; } @@ -395,39 +406,6 @@ p.time { } } -.btn { - &.very_small { - font-size:11px; - padding:2px 6px; - margin:2px; - } - - &.grouped { - margin-right:7px; - float:left; - } - - &.padded { - margin-right:3px; - padding:4px 10px 4px; - } -} - - -.prettyprint { - background-color: #fefbf3; - padding: 9px; - border: 1px solid rgba(0,0,0,.2); - -webkit-box-shadow: 0 1px 2px rgba(0,0,0,.1); - -moz-box-shadow: 0 1px 2px rgba(0,0,0,.1); - box-shadow: 0 1px 2px rgba(0,0,0,.1); -} - -.hint { - font-style: italic; - color: #999; -} - .upvotes { font-size: 14px; font-weight: bold; @@ -551,14 +529,6 @@ li.note { } -/** - * Milestones list - * - */ - -.milestone { - @extend .wll; -} /** * Admin area @@ -605,11 +575,10 @@ li.note { * */ .event_lp { - @extend .alert-info; + @extend .ui-box; + color:#777; margin-bottom:20px; padding:8px; - border-style: solid; - border-width: 1px; @include border-radius(4px); min-height:22px; @@ -623,88 +592,19 @@ li.note { cursor:pointer; } -/** - * Issues, MRs legend - * - */ - -.list_legend { - float:left; - margin-right:20px; - .icon { - width:12px; - height:12px; - float:left; - margin-right:5px; - margin-top: 2px; - @include border-radius(4px); - &.today{ - background: #ADA; - border:1px solid #8B8; - } - &.closed { - background: #DDD; - border:1px solid #BBB; - } - &.yours { - background: #AAD; - border:1px solid #88B; - } - &.merged { - background: #DAD; - border:1px solid #B8B; - } - } - .text { - padding-bottom: 10px; - float:left; - } -} - .merge_request, .issue { - .list_legend { - margin-right: 5px; - margin-top: 14px; - .icon { - width:8px; - height:8px; - float:left; - margin-right:5px; - @include border-radius(4px); - border:1px solid #ddd; - } - } - &.today{ background: #EFE; border-color:#CEC; - .icon { - background: #ADA; - border:1px solid #8B8; - } } &.closed { background: #F5f5f5; border-color:#E5E5E5; - .icon { - background: #DDD; - border:1px solid #BBB; - } - } - &.yours { - .icon { - background: #AAD; - border:1px solid #88B; - } } &.merged { background: #F5f5f5; border-color:#E5E5E5; - .icon { - background: #DAD; - border:1px solid #B8B; - } } } diff --git a/app/assets/stylesheets/gitlab_bootstrap.scss b/app/assets/stylesheets/gitlab_bootstrap.scss deleted file mode 100644 index a1faf0601cb..00000000000 --- a/app/assets/stylesheets/gitlab_bootstrap.scss +++ /dev/null @@ -1,817 +0,0 @@ -body { - margin-bottom:20px; -} - -pre { - font-family:'Menlo', 'Liberation Mono', 'Consolas', 'Courier New', 'andale mono','lucida console',monospace; - - &.dark { - background: #333; - color:#f5f5f5; - } -} - -a { - outline: none; - color: $link_color; - &:hover { - text-decoration:none; - color: $blue_link; - } - - &.btn { - color: $style_color; - } - - &.dark { - color: $style_color; - } - - &.lined { - text-decoration:underline; - &:hover { text-decoration:underline; } - } - - &.gray { - color:gray; - } - - &.supp_diff_link { - text-align:center; - padding:20px 0; - background:#f1f1f1; - width:100%; - float:left; - } - - &.neib { - margin-right:15px; - } -} - -.neib { - margin-right:10px; -} - -.alert-message { - @extend .alert; - - &.success { - @extend .alert-success; - } - - &.error { - @extend .alert-error; - } -} - -.alert { - &.alert-well { - background:#ddd; - border:1px solid #ccc; - background-image: -webkit-gradient(linear, 0 0, 0 30, color-stop(0.066, #ddd), to(#dfdfdf)); - background-image: -webkit-linear-gradient(#ddd 6.6%, #dfdfdf); - background-image: -moz-linear-gradient(#ddd 6.6%, #dfdfdf); - background-image: -o-linear-gradient(#ddd 6.6%, #dfdfdf); - color:#111; - } -} - -h3, h4, h5, h6 { - line-height: 36px; -} - -h5 { - font-size:14px; -} - - -table { - width:100%; - th { - padding-top: 9px; - font-weight: bold; - vertical-align: middle; - } - th, td { - padding: 10px 10px 9px; - line-height: 18px; - text-align: left; - } - - &.bordered-table { - border: 1px solid #DDD; - border-collapse: separate; - -webkit-border-radius: 4px; - -moz-border-radius: 4px; - border-radius: 4px; - } - - &.zebra-striped { - @extend .table-striped; - } -} - -.btn { - background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#f1f1f1), color-stop(25%, #f1f1f1), to(#e6e6e6)); - background-image: -webkit-linear-gradient(#f1f1f1, #f1f1f1 25%, #e6e6e6); - background-image: -moz-linear-gradient(top, #f1f1f1, #f1f1f1 25%, #e6e6e6); - background-image: -ms-linear-gradient(#f1f1f1, #f1f1f1 25%, #e6e6e6); - background-image: -o-linear-gradient(#f1f1f1, #f1f1f1 25%, #e6e6e6); - background-image: linear-gradient(#f1f1f1, #f1f1f1 25%, #e6e6e6); - - &:hover { - } - - &.btn-primary { - background:$link_color; - border-color: #2A79A3; - &:hover { - background:$blue_link; - } - } - &.primary { - @extend .btn-primary; - } - - &.success { - color: #fff; - text-shadow: 0 0 1px #111; - background: #5bb75b;; - font-weight: bold; - - &:hover { - background-color: #51a351; - color: #fff; - } - } - - &.danger, - &.btn-danger { - color:#fff; - background: #DA4E49; - border-color: #BD362F; - - &:hover { - color:#fff; - background: #EE4E49; - } - } - - &.danger { - @extend .btn-danger; - } - - &.small { - @extend .btn-small; - } - - &.active { - border-color:#aaa; - background-color:#ccc; - } -} - -a:focus { - outline: none; -} - -.nav-pills a:hover { - background-color:#888; -} - -.nav-pills .active a { - background-color: $style_color; -} - -.label { - background-color: #474D57; - &.label-important { - background-color: #B94A48; - } - - &.label-issue { - background-color: #eee; - border: 1px solid #ccc; - padding:4px 6px; - color:#444; - text-shadow:0 0 1px #fff; - - &.grouped { - float: left; - margin-right: 6px; - padding: 6px; - } - } -} - -.nav-tabs > li > a, .nav-pills > li > a { - color:$style_color; -} - -.nav-tabs > .active > a { - font-weight:bold; -} - -/** COLORS **/ -.cgray { color:gray; } -.cred { color:#D12F19; } -.cgreen { color:#44aa22; } -.cblack { color:#111; } -.cdark { color:#444 } -.cwhite { color:#fff !important } -.bgred { background: #F2DEDE !important} - -/** COMMON STYLES **/ -.left { - float:left; -} -.right { - float:right !important; -} -.width-50p{ - width:50%; -} -.width-49p{ - width:49%; -} -.width-30p{ - width:30%; -} -.width-65p{ - width:65%; -} -.width-100p{ - width:100%; -} -.append-bottom-10 { - margin-bottom:10px; -} -.append-bottom-20 { - margin-bottom:20px; -} -.prepend-top-10 { - margin-top:10px; -} - -.prepend-top-20 { - margin-top:20px; -} - -.padded { - padding:20px; -} - -.ipadded { - padding:20px !important; -} -.lborder { - border-left:1px solid #eee; -} - -.borders { - border: 1px solid #ccc; - @include shade; -} -.no-borders { - border:none; -} -table.no-borders { - border:none; - tr, td { border:none } -} -.no-padding { - padding:0 !important; -} -.underlined { - border-bottom: 1px solid $border_color; -} -.vlink { - color: $link_color !important; -} - -.pretty_label { - @include round-borders-all(4px); - padding:2px 4px; - background-image: -webkit-gradient(linear, 0 0, 0 26, color-stop(0.076, #fefefe), to(#F6F7F8)); - background-image: -webkit-linear-gradient(#fefefe 7.6%, #F6F7F8); - background-image: -moz-linear-gradient(#fefefe 7.6%, #F6F7F8); - background-image: -o-linear-gradient(#fefefe 7.6%, #F6F7F8); - color: #777; - border: 1px solid #DEDFE1; - - &.branch { - border:none; - font-size:13px; - background: #474D57; - color:#fff; - font-weight:bold; - font-family: monospace; - } -} - -.event_label { - @extend .label; - background-color: #999; - - &.pushed { - background-color: #3A87AD; - } - - &.opened { - background-color: #468847; - } - - &.closed { - background-color: #B94A48; - } - - &.merged { - background-color: #2A2; - } -} - -img.avatar { - float:left; - margin-right:15px; - width:40px; - border:1px solid #ddd; - padding:1px; - - &.s16 { - width:16px; - height:16px; - } - &.s24 { - width:24px; - height:24px; - } - &.s32 { - width:32px; - height:32px; - } -} - -img.lil_av { - padding-left: 4px; - padding-right:3px; -} - -form { - @extend .form-horizontal; - - .actions { - @extend .form-actions; - } - - .clearfix { - @extend .control-group; - } - - .input { - @extend .controls; - } - - label { - @extend .control-label; - } - .xlarge { - @extend .input-xlarge; - } - .xxlarge { - @extend .input-xxlarge; - } -} - -/** - * List li block element #1 - * - */ -.wll { - background-color: #FFF; - padding: 10px 5px; - min-height: 20px; - border-bottom: 1px solid #eee; - border-bottom: 1px solid rgba(0, 0, 0, 0.05); - &.smoke { - background-color:#f5f5f5; - } - &:hover { - background:$hover; - } - &:last-child { border:none } - p { padding-top:5px; margin:0; color:$style_color;} - .author { color: #999; } - p { - color:#222; - margin-bottom: 0; - img { - position:relative; - top:3px; - } - } -} - - -/** - * Block element #2 - * - */ -.entry { - position: relative; - padding: 7px 15px; - margin-bottom: 18px; - color: #404040; - filter:none; - - text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25); - text-shadow: 0 1px 0 rgba(255, 255, 255, 0.5); - - -webkit-border-radius: 4px; - -moz-border-radius: 4px; - border-radius: 4px; - - -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.25); - -moz-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.25); - box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.25); - - background:#F1F1F1; - border: 1px solid #ccc; - - - p { - color:$style_color; - margin-bottom: 0; - img { - position:relative; - top:3px; - } - } -} - - -/** - * Big UI Block for show page content - * - */ -.ui-box { - background:#F9F9F9; - margin-bottom: 25px; - @include round-borders-all(4px); - border-color: #CCC; - @include solid_shade; - - ul { - margin:0; - } - - h5, .title { - padding: 0 10px; - @include round-borders-top(4px); - @include bg-gray-gradient; - border-bottom: 1px solid #bbb; - - &.small { - line-height: 28px; - font-size: 14px; - line-height:28px; - text-shadow: 0 1px 1px white; - } - - form { - padding:9px 0; - margin:0px; - } - - .nav-pills { - li { - padding:3px 0; - &.active a { background-color:$style_color; } - a { - border-radius:7px; - } - } - } - } - - .bottom { - @include bg-gray-gradient; - @include round-borders-bottom(4px); - border-bottom:none; - border-top: 1px solid #bbb; - } - - &.padded { - h5, .title { - margin: -20px; - margin-bottom: 0; - padding: 5px 20px; - } - .middle_title { - background:#f5f5f5; - margin:20px -20px; - padding: 0 20px; - border-top:1px solid #eee; - border-bottom:1px solid #eee; - font-size:14px; - color:#777; - } - } - .row_title { - font-weight:bold; - color:#444; - &:hover { - color:#444; - text-decoration:underline; - } - } - - li, .wll { - padding:10px; - &:first-child { - @include round-borders-top(4px); - border-top:none; - } - - &:last-child { - @include round-borders-bottom(4px); - border:none; - } - } - -} - -table.admin-table { - @extend .table-bordered; - @extend .zebra-striped; - @include solid_shade; - th { - border-color: #CCC; - border-bottom: 1px solid #bbb; - @include bg-gray-gradient; - } -} - -.field_with_errors { - display:inline; -} - -ul.breadcrumb { - background:white; - border:none; - li { - display: inline; - text-shadow: 0 1px 0 white - } - - a { - color:#474D57; - font-weight:bold; - font-size:14px; - } - - .arrow { - background: url("images.png") no-repeat -85px -77px; - width: 19px; - height: 16px; - float: left; - position: relative; - left: -10px; - padding:0; - margin:0; - } -} - -.nothing_here_message { - text-align:center; - padding:20px; - color:#777; -} - -/** - * UI box element - * contains top, middle, bottom blocks - * - */ -.main_box { - @extend .borders; - @extend .prepend-top-20; - @extend .append-bottom-20; - border-width:1px; - @include solid_shade; - - - img { max-width: 100%; } - - pre { - code { - background: none !important; - } - } - - .top_box_content, - .middle_box_content, - .bottom_box_content { - padding:15px; - - pre { - background: none !important; - margin:0; - border:none; - padding:0; - } - } - - .middle_box_content { - border-radius:0; - border:none; - font-size:12px; - background-color:#f5f5f5; - border:none; - border-top:1px solid #eee; - } - - .bottom_box_content { - border-top:1px solid #eee; - } -} - -input[type=text] { - &.large_text { - padding:6px; - font-size:16px; - } -} - -p { - &.slead { - color:#456; - font-size:16px; - margin-bottom: 12px; - font-weight: 200; - line-height: 24px; - } -} - -h3.page_title { - color:#456; - font-size:20px; - font-weight: normal; - line-height: 28px; -} - -/** - * File content holder - * - */ -.file_holder { - border:1px solid #CCC; - margin-bottom:1em; - @include solid_shade; - - .file_title { - border-bottom: 1px solid #bbb; - @include bg-gray-gradient; - 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/gitlab_bootstrap/blocks.scss b/app/assets/stylesheets/gitlab_bootstrap/blocks.scss new file mode 100644 index 00000000000..894cb3044fa --- /dev/null +++ b/app/assets/stylesheets/gitlab_bootstrap/blocks.scss @@ -0,0 +1,145 @@ +/** + * =================================== + * Contain 3 main UI block elements: + * .main_box - for show pages + * .ui-box - for simple block & widgets + * =================================== + */ + +/** + * UI box element + * contains top, middle, bottom blocks + * + */ +.main_box { + @extend .borders; + @extend .prepend-top-20; + @extend .append-bottom-20; + border-width:1px; + @include solid_shade; + + + img { max-width: 100%; } + + pre { + code { + background: none !important; + } + } + + .top_box_content, + .middle_box_content, + .bottom_box_content { + padding:15px; + + pre { + background: none !important; + margin:0; + border:none; + padding:0; + } + } + + .middle_box_content { + border-radius:0; + border:none; + font-size:12px; + background-color:#f5f5f5; + border:none; + border-top:1px solid #eee; + } + + .bottom_box_content { + border-top:1px solid #eee; + } +} + +/** + * Big UI Block for show page content + * + */ +.ui-box { + background:#F9F9F9; + margin-bottom: 25px; + @include round-borders-all(4px); + border-color: #CCC; + @include solid_shade; + + ul { + margin:0; + } + + h5, .title { + padding: 0 10px; + @include round-borders-top(4px); + @include bg-gray-gradient; + border-bottom: 1px solid #bbb; + + &.small { + line-height: 28px; + font-size: 14px; + line-height:28px; + text-shadow: 0 1px 1px white; + } + + form { + padding:9px 0; + margin:0px; + } + + .nav-pills { + li { + padding:3px 0; + &.active a { background-color:$style_color; } + a { + border-radius:7px; + } + } + } + } + + .bottom { + @include bg-gray-gradient; + @include round-borders-bottom(4px); + border-bottom:none; + border-top: 1px solid #bbb; + } + + &.padded { + h5, .title { + margin: -20px; + margin-bottom: 0; + padding: 5px 20px; + } + .middle_title { + background:#f5f5f5; + margin:20px -20px; + padding: 0 20px; + border-top:1px solid #eee; + border-bottom:1px solid #eee; + font-size:14px; + color:#777; + } + } + .row_title { + font-weight:bold; + color:#444; + &:hover { + color:#444; + text-decoration:underline; + } + } + + li, .wll { + padding:10px; + &:first-child { + @include round-borders-top(4px); + border-top:none; + } + + &:last-child { + @include round-borders-bottom(4px); + border:none; + } + } +} diff --git a/app/assets/stylesheets/gitlab_bootstrap/buttons.scss b/app/assets/stylesheets/gitlab_bootstrap/buttons.scss new file mode 100644 index 00000000000..c838f3b2368 --- /dev/null +++ b/app/assets/stylesheets/gitlab_bootstrap/buttons.scss @@ -0,0 +1,105 @@ +.btn { + background-image: -webkit-gradient(linear, 0 0, 0 26, color-stop(0.076, #f7f7f7), to(#d5d5d5)); + background-image: -webkit-linear-gradient(#f7f7f7 7.6%, #d5d5d5); + background-image: -moz-linear-gradient(#f7f7f7 7.6%, #d5d5d5); + background-image: -o-linear-gradient(#f7f7f7 7.6%, #d5d5d5); + border-color:#aaa; + &:hover { + @include bg-gray-gradient; + border-color:#bbb; + color:#333; + } + + &.primary { + background:#2a79A3; + border-color: #2A79A3; + background-image: -webkit-linear-gradient(#47A7b7 7.6%, #2585b5); + background-image: -moz-linear-gradient(#47A7b7 7.6%, #2585b5); + background-image: -o-linear-gradient(#47A7b7 7.6%, #2585b5); + color:#fff; + text-shadow: 0 1px 1px #268; + &:hover { + background:$blue_link; + color:#fff; + } + + &.disabled { + color:#fff; + background:#29B; + } + } + + &.success { + border-color: #4A4; + background-image: -webkit-linear-gradient(#82D482 7.6%, #22B442); + background-image: -moz-linear-gradient(#82D482 7.6%, #22B442); + background-image: -o-linear-gradient(#82D482 7.6%, #22B442); + color: #fff; + text-shadow: 0 1px 1px #141; + + &:hover { + background: #6C6; + color: #fff; + } + + &.disabled { + color:#fff; + background:#2b2; + } + } + + &.save-btn { + @extend .wide; + @extend .primary; + } + + &.cancel-btn { + float:right; + } + + &.wide { + padding-left:30px; + padding-right:30px; + } + + &.danger, + &.btn-danger { + color:#fff; + background: #DA4E49; + border-color: #BD362F; + + &:hover { + color:#fff; + background: #EE4E49; + } + } + + &.danger { + @extend .btn-danger; + } + + &.small { + @extend .btn-small; + } + + &.active { + border-color:#aaa; + background-color:#ccc; + } + + &.very_small { + font-size:11px; + padding:2px 6px; + margin:2px; + } + + &.grouped { + margin-right:7px; + float:left; + } + + &.padded { + margin-right:3px; + padding:4px 10px 4px; + } +} diff --git a/app/assets/stylesheets/gitlab_bootstrap/common.scss b/app/assets/stylesheets/gitlab_bootstrap/common.scss new file mode 100644 index 00000000000..cd7145c99c3 --- /dev/null +++ b/app/assets/stylesheets/gitlab_bootstrap/common.scss @@ -0,0 +1,52 @@ +/** COLORS **/ +.cgray { color:gray } +.cred { color:#D12F19 } +.cgreen { color:#4a2 } +.cblack { color:#111 } +.cdark { color:#444 } +.cwhite { color:#fff!important } +.bgred { background:#F2DEDE!important } + +/** COMMON CLASSES **/ +.left { float:left } +.right { float:right!important } +.width-50p { width:50% } +.width-49p { width:49% } +.width-30p { width:30% } +.width-65p { width:65% } +.width-100p { width:100% } +.append-bottom-10 { margin-bottom:10px } +.append-bottom-20 { margin-bottom:20px } +.prepend-top-10 { margin-top:10px } +.prepend-top-20 { margin-top:20px } +.padded { padding:20px } +.ipadded { padding:20px!important } +.lborder { border-left:1px solid #eee } +.no-padding { padding:0 !important; } +.underlined { border-bottom: 1px solid #CCC; } +.no-borders { border:none; } +.vlink { color: $link_color !important; } +.borders { border: 1px solid #ccc; @include shade; } +.hint { font-style: italic; color: #999; } + +/** PILLS & TABS**/ +.nav-pills a:hover { background-color:#888; } +.nav-pills .active a { background-color: $style_color; } +.nav-tabs > li > a, .nav-pills > li > a { color:$style_color; } +.nav-tabs > .active > a { font-weight:bold; } + +/** ALERT MESSAGES **/ +.alert-message { @extend .alert; } +.alert-messag.success { @extend .alert-success; } +.alert-message.error { @extend .alert-error; } + +/** AVATARS **/ +img.avatar { float:left; margin-right:15px; width:40px; border:1px solid #ddd; padding:1px; } +img.avatar.s16 { width:16px; height:16px; } +img.avatar.s24 { width:24px; height:24px; } +img.avatar.s32 { width:32px; height:32px; } +img.lil_av { padding-left: 4px; padding-right:3px; } + +/** HELPERS **/ +.nothing_here_message { text-align:center; padding:20px; color:#777; } +p.slead { color:#456; font-size:16px; margin-bottom: 12px; font-weight: 200; line-height: 24px; } diff --git a/app/assets/stylesheets/gitlab_bootstrap/files.scss b/app/assets/stylesheets/gitlab_bootstrap/files.scss new file mode 100644 index 00000000000..4ea5ed02ada --- /dev/null +++ b/app/assets/stylesheets/gitlab_bootstrap/files.scss @@ -0,0 +1,156 @@ +/** + * File content holder + * + */ +.file_holder { + border:1px solid #CCC; + margin-bottom:1em; + @include solid_shade; + + .file_title { + border-bottom: 1px solid #bbb; + @include bg-gray-gradient; + 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/gitlab_bootstrap/lists.scss b/app/assets/stylesheets/gitlab_bootstrap/lists.scss new file mode 100644 index 00000000000..402ba04bd00 --- /dev/null +++ b/app/assets/stylesheets/gitlab_bootstrap/lists.scss @@ -0,0 +1,30 @@ +/** LISTS **/ + +ul { + /** + * List li block element #1 + * + */ + .wll { + background-color: #FFF; + padding: 10px 5px; + min-height: 20px; + border-bottom: 1px solid #eee; + border-bottom: 1px solid rgba(0, 0, 0, 0.05); + + &.smoke { background-color:#f5f5f5; } + &:hover { background:$hover; } + &:last-child { border:none } + .author { color: #999; } + + p { + padding-top:5px; + margin:0; + color:#222; + img { + position:relative; + top:3px; + } + } + } +} diff --git a/app/assets/stylesheets/gitlab_bootstrap/tables.scss b/app/assets/stylesheets/gitlab_bootstrap/tables.scss new file mode 100644 index 00000000000..f78b1dee4fa --- /dev/null +++ b/app/assets/stylesheets/gitlab_bootstrap/tables.scss @@ -0,0 +1,41 @@ +table { + width:100%; + th { + padding-top: 9px; + font-weight: bold; + vertical-align: middle; + } + th, td { + padding: 10px 10px 9px; + line-height: 18px; + text-align: left; + } + + &.bordered-table { + border: 1px solid #DDD; + border-collapse: separate; + -webkit-border-radius: 4px; + -moz-border-radius: 4px; + border-radius: 4px; + } + + &.zebra-striped { + @extend .table-striped; + } +} + +table.admin-table { + @extend .table-bordered; + @extend .zebra-striped; + @include solid_shade; + th { + border-color: #CCC; + border-bottom: 1px solid #bbb; + @include bg-gray-gradient; + } +} + +table.no-borders { + border:none; + tr, td { border:none } +} diff --git a/app/assets/stylesheets/gitlab_bootstrap/typography.scss b/app/assets/stylesheets/gitlab_bootstrap/typography.scss new file mode 100644 index 00000000000..97e85492875 --- /dev/null +++ b/app/assets/stylesheets/gitlab_bootstrap/typography.scss @@ -0,0 +1,71 @@ +/** + * Headers + * + */ +h3, h4, h5, h6 { line-height: 36px; } +h5 { font-size:14px; } +h3.page_title { + color:#456; + font-size:20px; + font-weight: normal; + line-height: 28px; +} + +/** CODE **/ +pre { + font-family:'Menlo', 'Liberation Mono', 'Consolas', 'Courier New', 'andale mono','lucida console',monospace; + + &.dark { + background: #333; + color:#f5f5f5; + } +} + +/** + * Links + * + */ +a { + outline: none; + color: $link_color; + &:hover { + text-decoration:none; + color: $blue_link; + } + + &.btn { + color: $style_color; + &:hover { + color: $style_color; + } + } + + &.dark { + color: $style_color; + } + + &.lined { + text-decoration:underline; + &:hover { text-decoration:underline; } + } + + &.gray { + color:gray; + } + + &.supp_diff_link { + text-align:center; + padding:20px 0; + background:#f1f1f1; + width:100%; + float:left; + } + + &.neib { + margin-right:15px; + } +} + +a:focus { + outline: none; +} diff --git a/app/assets/stylesheets/main.scss b/app/assets/stylesheets/main.scss index b4f0ebf8ba0..be27d754dfa 100644 --- a/app/assets/stylesheets/main.scss +++ b/app/assets/stylesheets/main.scss @@ -2,29 +2,13 @@ @import "bootstrap-responsive"; /** GITLAB colors **/ -$text_color:#222; -$lite_text_color: #666; -$link_color:#2A79A3; -$active_link_color:#2FA0BB; -$active_bg_color:#79C3E0; -$active_bd_color: #2FA0BB; -$border_color:#CCC; -$lite_border_color:#EEE; -$min_app_width:980px; -$max_app_width:980px; -$app_padding:20px; -$bg_color: #FFF; -$styled_border_color: #2FA0BB; -$color: "#4BB8D2"; +$link_color:#3A89A3; $blue_link: #2fa0bb; - - -/** Style colors **/ -$style_color: #474D57; -$hover: #FDF5D9; +$style_color: #474d57; +$hover: #fdf5d9; /** GITLAB Fonts **/ -@font-face { font-family: Korolev; src: url('korolev-medium-compressed.otf'); } +@font-face { font-family: Korolev; src: url('korolev-medium-compressed.otf'); } /** MIXINS **/ @mixin shade { @@ -72,7 +56,7 @@ $hover: #FDF5D9; border-radius: $radius; } -@mixin bg-gray-gradient { +@mixin bg-gray-gradient { 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); @@ -80,6 +64,13 @@ $hover: #FDF5D9; background-image: -o-linear-gradient(#eee 6.6%, #dfdfdf); } +@mixin bg-dark-gray-gradient { + background:#eee; + background-image: -webkit-linear-gradient(#e9e9e9, #d7d7d7); + background-image: -moz-linear-gradient(#e9e9e9, #d7d7d7); + background-image: -o-linear-gradient(#e9e9e9, #d7d7d7); +} + /** * Header of application. * Contain application logo, search panel, profile icon @@ -119,7 +110,13 @@ $hover: #FDF5D9; * Overrides some styles of twitter bootstrap. * Also give some common classes for gitlab app */ -@import "gitlab_bootstrap.scss"; +@import "gitlab_bootstrap/common.scss"; +@import "gitlab_bootstrap/typography.scss"; +@import "gitlab_bootstrap/buttons.scss"; +@import "gitlab_bootstrap/blocks.scss"; +@import "gitlab_bootstrap/files.scss"; +@import "gitlab_bootstrap/tables.scss"; +@import "gitlab_bootstrap/lists.scss"; /** diff --git a/app/assets/stylesheets/projects.css.scss b/app/assets/stylesheets/projects.css.scss deleted file mode 100644 index 4bdf5dee2f5..00000000000 --- a/app/assets/stylesheets/projects.css.scss +++ /dev/null @@ -1,385 +0,0 @@ -.git_url_wrapper { margin-right:50px } - -.sidebar aside a{ - display: block; - position: relative; - padding: 15px 10px; - margin: 10px 0 0 0; - - font-size:13px; - font-weight:bold; - color:#333; - - &.current { - color: white; - background: $active_bg_color; - border: 1px solid $active_bd_color; - border-radius:5px; - - -webkit-border-top-right-radius: 0; - -webkit-border-bottom-right-radius: 0; - -moz-border-radius-topright: 0px; - -moz-border-radius-bottomright: 0px; - border-top-right-radius: 0; - border-bottom-right-radius: 0; - margin-right: -1px; - } -} - -body table .commit a{color: #{$blue_link}} -body table th, body table td{ border-bottom: 1px solid #DEE2E3;} -body .fixed{position: fixed; } - -/** File stat **/ -.file_stats { - span { - img { - width:14px; - float:left; - margin-right: 6px; - padding:2px 0; - } - } -} - -.round-borders { - @include round-borders-all(4px); - padding: 4px 0px; -} - -table.round-borders { - float:left; - text-align: left; -} - - - -/** PROJECTS **/ -input.ssh_project_url { - padding:5px; - margin:0px; - float:right; - width:400px; - text-align:center; -} - -#projects-list .project { - height:50px; -} - -#tree-slider .tree-item, -#projects-list .project, -#snippets-table .snippet, -#issues-table .issue{ - cursor:pointer; -} - -.clear { - clear: both; -} - - - -#user_projects_limit{ - width: 60px; -} - -.handle:hover{ - cursor: move; -} - -.project-refs-form { - span { - background: none !important; - position:static !important; - width:auto !important; - height: auto !important; - } -} - -.project-refs-select { - width:200px; -} - -.filter .left { margin-right:15px; } - -body table .commit { - a.tree-commit-link { - color:#444; - &:hover { - text-decoration:underline; - } - } -} - -/** NEW PROJECT **/ -.new-project-hodler { - .icon span { background-position: -31px -70px; } - td { border-bottom: 1px solid #DEE2E3; } -} - -/** Feed entry **/ -.commit, -.snippet, -.message { - .title { - color:#666; - a { color:#666 !important; } - p { margin-top:0px; } - } - .author { color: #999 } -} - -/** JQuery UI **/ -.ui-autocomplete { @include round-borders-all(5px); } -.ui-menu-item { cursor: pointer } -.ui-selectmenu{ - @include round-borders-all(4px); - margin-right:10px; - font-size:1.5em; - height:auto; - font-weight:bold; - .ui-selectmenu-status { - padding:3px 10px; - } -} - -#holder { - background:#FAFAFA; - border: 1px solid #EEE; - cursor: move; - height: 70%; - overflow: hidden; -} - -/* Project Dashboard Page */ -html, body { height: 100%; } - -.news-feed h2{float: left;} -.news-feed .project-updates {margin-bottom: 20px; display: block; width: 100%;} -.news-feed .project-updates .data{ padding: 0} -.news-feed .project-updates a.project-update {padding: 10px; border-bottom: 1px solid #eee; overflow: hidden; display: block;} -.news-feed .project-updates a.project-update:last-child{border-bottom: 0} -.news-feed .project-updates a.project-update img{float: left; margin-right: 10px;} -.news-feed .project-updates a.project-update span.update-title, .dashboard-page .news-feed .project-updates li a span.update-author{display: block;} -.news-feed .project-updates a.project-update span.update-title{margin-bottom: 10px} -.news-feed .project-updates a.project-update span.update-author{color: #999; font-weight: normal; font-style: italic;} -.news-feed .project-updates a.project-update span.update-author strong{font-weight: bold; font-style: normal;} -/* eo Dashboard Page */ - - -/** Update entry **/ -.update-data { padding: 0 } -.update-data { width:100%; } -.update-data.ui-box .data { padding:0; } -a.update-item {padding: 10px; border-bottom: 1px solid #eee; overflow: hidden; display: block;} -a.update-item:last-child{border-bottom: 0} -a.update-item img{float: left; margin-right: 10px;} -a.update-item span.update-title, .dashboard-page .news-feed .project-updates li a span.update-author{display: block;} -a.update-item span.update-title{margin-bottom: 10px} -a.update-item span.update-author{color: #999; font-weight: normal; font-style: italic;} -a.update-item span.update-author strong{font-weight: bold; font-style: normal;} - - -body .team_member_new .span-6, .team_member_edit .span-6{ padding:10px 0; } - -body.projects-page input.text.git-url.project_list_url { width:165px; } - - -body table.no-borders th { - background:none; - border-bottom:1px solid #CCC; - color:#333; -} - -body table.no-borders tr, -body table.no-borders td{ - border:none; -} - -.ajax-tab-loading { - padding:40px; - display:none; -} - -#tree-content-holder { float:left; width:100%; } - -#tree-readme-holder { - float:left; - width:100%; - - .readme { - @include round-borders-all(4px); - padding: 4px 15px; - background:#F7F7F7; - } -} - - - -/* Commit Page */ -.entity-info {float: right;} -.entity-button{ - background-image: -webkit-gradient(linear, 0 0, 0 26, color-stop(0.192, #fff), to(#f4f4f4)); - background-image: -webkit-linear-gradient(#fff 19.2%, #f4f4f4); - background-image: -moz-linear-gradient(#fff 19.2%, #f4f4f4); - background-image: -o-linear-gradient(#fff 19.2%, #f4f4f4); - box-shadow: 0 -1px 0 white inset; - display: block; - border: 1px solid #eee; - border-radius: 5px; - margin-bottom: 2px; - position: relative; - padding: 4px 10px; - font-size: 11px; - padding-right: 20px; -} - -.entity-button i{ - background: url('images.png') no-repeat -138px -27px; - width: 6px; - height: 9px; - float: right; - position: absolute; - top: 6px; - right: 5px; -} -.box-arrow{float: right; background: #E3E5EA; padding: 10px; border-radius: 5px; margin-top: 2px; text-shadow: none; color: #999; margin: 1.5em 0;} - -h4.dash-tabs { - margin: 0; - border-bottom: 1px solid #ccc; - padding: 10px 10px; - font-size: 11px; - padding-left:20px; - font-weight: bold; text-transform: uppercase; - background: #F7F7F7; - margin-bottom:20px; - height:13px; - -} - -.dash-button { - border-right: 1px solid #ddd; - background:none; - padding: 10px 15px; - float:left; - position:relative; - top:-10px; - left:0px; - height:13px; - - &:first-child { - border-left: 1px solid #ddd; - } - &.active { - background: #eaeaea; - } -} - - -.dashboard-loader { - float:right; - margin-right:30px; - display:none; -} - - -.merge-tabs { - margin: 0; - border: 1px solid #ccc; - padding: 5px; - font-size: 12px; - background: #F7F7F7; - margin-bottom:20px; - height:26px; - - -moz-border-radius: 4px; - -webkit-border-radius: 4px; - border-radius: 4px; - - .tab { - font-weight: bold; - border-right: 1px solid #ddd; - background:none; - padding: 10px; - min-width:60px; - float:left; - position:relative; - top:-5px; - left:-5px; - height:16px; - padding-left:34px; - - span { - width: 20px; - height: 20px; - display: inline-block; - position: absolute; - left: 8px; - top: 8px; - } - - &.active { - background: #eaeaea; - } - } -} -.merge-tabs.repository .tab span{ background: url("images.png") no-repeat -38px -77px; } -.activities-tab span { background: url("images.png") no-repeat -161px -1px; } -.stat-tab span, -.team-tab span, -.snippets-tab span { background: url("images.png") no-repeat -38px -77px; } -.files-tab span { background: url("images.png") no-repeat -112px -23px; } - -.merge-notes-tab span { background: url("images.png") no-repeat -161px -1px; } -.merge-commits-tab span { background: url("images.png") no-repeat -86px 1px; } -.merge-diffs-tab span { background: url("images.png") no-repeat -118px 1px; } -.merge-tabs .dashboard-loader { padding:8px; } - -.user-mention { - color: #2FA0BB; - font-weight: bold; -} - -.author { - color: #999; -} - - - - -.dark_scheme_box { - padding:20px 0; - - label { - float:left; - box-shadow: 0 0px 5px rgba(0,0,0,.3); - - img { - } - } -} - -a.project-update.titled { - position: relative; - padding-left: 235px !important; - - .title-block { - padding: 10px; - width: 205px; - position: absolute; - left: 0; - top: 0; - } -} - -.add_new { - float: right; - background: #A6B807; - color: white; - padding: 4px 10px; - @include round-borders-all(4px); - font-size:11px; - margin: 10px 0; -} diff --git a/app/assets/stylesheets/sections/commits.scss b/app/assets/stylesheets/sections/commits.scss index e2db701db71..75e38aeea93 100644 --- a/app/assets/stylesheets/sections/commits.scss +++ b/app/assets/stylesheets/sections/commits.scss @@ -206,4 +206,24 @@ min-width:65px; font-family: 'Menlo', 'Liberation Mono', 'Consolas', 'Courier New', 'andale mono','lucida console',monospace; } + + .commit-author-name { + color: #777; + } +} + +.diff_file_header a, +.file_stats a { + color:$style_color; +} + +.file_stats { + span { + img { + width:14px; + float:left; + margin-right:6px; + padding:2px 0; + } + } } diff --git a/app/assets/stylesheets/sections/issues.scss b/app/assets/stylesheets/sections/issues.scss index 1b61ec3fe85..230a7aea1da 100644 --- a/app/assets/stylesheets/sections/issues.scss +++ b/app/assets/stylesheets/sections/issues.scss @@ -65,6 +65,11 @@ input.check_all_issues { } } +@media (min-width: 800px) { .issues_filters select { width:160px; } } +@media (min-width: 1000px) { .issues_filters select { width:200px; } } +@media (min-width: 1200px) { .issues_filters select { width:220px; } } + + #issues-table-holder { .issues_filters { form { @@ -99,3 +104,11 @@ input.check_all_issues { #update_status { width:100px; } + +/** + * Milestones list + * + */ +.milestone { + @extend .wll; +} diff --git a/app/assets/stylesheets/sections/merge_requests.scss b/app/assets/stylesheets/sections/merge_requests.scss index ec84a64e23a..73171915fcc 100644 --- a/app/assets/stylesheets/sections/merge_requests.scss +++ b/app/assets/stylesheets/sections/merge_requests.scss @@ -11,23 +11,6 @@ background:#f1f1f1; } - .commit { - margin:0; - padding:0; - padding: 5px; - margin-bottom: 5px; - - .committed_ago { - display:none; - } - .browse_code_link_holder { - display:none; - } - list-style:none; - &:hover { - background:none; - } - } } /** @@ -55,6 +38,7 @@ background: #CEB; .accept_merge_request { + font-size:13px; float:left; } .remove_branch_holder { @@ -99,3 +83,42 @@ li.merge_request { @extend .padded; @extend .append-bottom-10; } + +.label_branch { + @include round-borders-all(4px); + padding:2px 4px; + border:none; + font-size:13px; + background: #474D57; + color:#fff; + font-weight:bold; + font-family: monospace; +} + +.mr_source_commit, +.mr_target_commit { + .commit { + margin:0; + padding:0; + padding: 5px; + margin-bottom: 5px; + .avatar { position:relative } + .row_title { + color:#444; + } + .commit-author-name, + .dash, + .committed_ago, + .browse_code_link_holder { + display:none; + } + list-style:none; + &:hover { + background:none; + } + } +} + +.mr_direction_tip { + margin-top:40px +} diff --git a/app/assets/stylesheets/sections/nav.scss b/app/assets/stylesheets/sections/nav.scss index fc7293b2864..097e819732d 100644 --- a/app/assets/stylesheets/sections/nav.scss +++ b/app/assets/stylesheets/sections/nav.scss @@ -6,7 +6,7 @@ ul.main_menu { border-radius: 4px; margin: auto; margin:30px 0; - border:1px solid #bbb; + border:1px solid #AAA; height:37px; @include bg-gray-gradient; position:relative; @@ -85,7 +85,7 @@ ul.main_menu { line-height:36px; color: $style_color; text-shadow:0 1px 1px white; - + padding:0 10px; } } /* diff --git a/app/assets/stylesheets/sections/notes.scss b/app/assets/stylesheets/sections/notes.scss index afa5095267d..6a965fa47b9 100644 --- a/app/assets/stylesheets/sections/notes.scss +++ b/app/assets/stylesheets/sections/notes.scss @@ -2,7 +2,7 @@ * Notes * */ -#notes-list, +#notes-list, #new_notes_list { display:block; list-style:none; @@ -10,7 +10,7 @@ padding:0px; } -#new_notes_list li:last-child{ +#new_notes_list li:last-child{ border-bottom:1px solid #aaa; } @@ -30,20 +30,24 @@ } #new_note { - #note_note { - height:25px; + .note-text { + height:40px; } .attach_holder { display:none; } } -#preview-note { - margin-bottom: 0; +.preview_note { + margin: 2px; + border: 1px solid #ddd; + padding: 10px; + min-height: 60px; + background:#f5f5f5; } -.note { - padding: 8px 0; +.note { + padding: 8px 0; border-bottom: 1px solid #eee; overflow: hidden; display: block; @@ -53,16 +57,16 @@ .note-author { color: $style_color;} .note-title { margin-left:45px; padding-top: 5px;} - .avatar { + .avatar { margin-top:3px; } - .delete-note { - display:none; + .delete-note { + display:none; float:right; } - &:hover { + &:hover { .delete-note { display:block; } } } @@ -76,18 +80,18 @@ p.notify_controls span{ font-weight: 700; } -tr.line_notes_row { +tr.line_notes_row { border-bottom:1px solid #DDD; border-left: 7px solid #2A79A3; - &.reply { + &.reply { background:#eee; border-left: 7px solid #2A79A3; border-top:1px solid #ddd; - td { + td { padding:7px 10px; } - a.line_note_reply_link { + a.line_note_reply_link { @include round-borders-all(4px); padding: 3px 10px; margin-left:5px; @@ -96,9 +100,9 @@ tr.line_notes_row { border-color: #2A79A3; } } - ul { + ul { margin:0; - li { + li { padding:0; border:none; } @@ -107,26 +111,26 @@ tr.line_notes_row { .line_notes_row, .per_line_form { font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; } -.per_line_form { +.per_line_form { background:#f5f5f5; border-top:1px solid #eee; form { margin: 0; } - td { + td { border-bottom:1px solid #ddd; } - .note_actions { + .note_actions { margin:0; padding-top: 10px; - .buttons { + .buttons { float:left; width:300px; } - .options { - .labels { + .options { + .labels { float:left; padding-left:10px; - label { + label { padding: 6px 0; margin: 0; width:120px; @@ -136,7 +140,7 @@ tr.line_notes_row { } } -td .line_note_link { +td .line_note_link { position:absolute; margin-left:-70px; margin-top:-10px; @@ -148,14 +152,14 @@ td .line_note_link { opacity: 0.0; filter: alpha(opacity=0); - &:hover { + &:hover { opacity: 1.0; filter: alpha(opacity=100); } } .diff_file_content tr.line_holder:hover > td { background: $hover !important; } -.diff_file_content tr.line_holder:hover > td .line_note_link { +.diff_file_content tr.line_holder:hover > td .line_note_link { opacity: 1.0; filter: alpha(opacity=100); } @@ -173,8 +177,8 @@ td .line_note_link { margin: 0; } - .note_advanced_opts { - h6 { + .note_advanced_opts { + h6 { line-height: 32px; padding-right: 15px; } @@ -187,7 +191,7 @@ td .line_note_link { overflow:hidden; margin:0 0 5px !important; - .input_file { + .input_file { .file_upload { position: absolute; right:14px; @@ -200,7 +204,7 @@ td .line_note_link { height:28px; overflow:hidden; } - .input-file { + .input-file { width: 260px; height: 41px; float: right; @@ -208,3 +212,8 @@ td .line_note_link { } } } + +.note-text { + border: 1px solid #aaa; + box-shadow:none; +} diff --git a/app/assets/stylesheets/sections/projects.scss b/app/assets/stylesheets/sections/projects.scss index 20fd2cbfe42..721b569d446 100644 --- a/app/assets/stylesheets/sections/projects.scss +++ b/app/assets/stylesheets/sections/projects.scss @@ -14,6 +14,32 @@ text-shadow: 0 1px 1px #fff; padding: 2px 10px; } + ul { + li { + padding:0; + a { + display:block; + .project_name { + color:#4fa2bd; + font-size:14px; + line-height:18px; + } + .arrow { + float:right; + padding:10px; + margin:0; + } + .last_activity { + padding-top:5px; + display:block; + span, strong { + font-size:12px; + color:#666; + } + } + } + } + } @extend .leftbar; @extend .ui-box; } @@ -33,11 +59,14 @@ color:#888; } .btn { - padding:6px; + padding:6px 10px; margin-left:10px; margin-bottom:8px; } } + .adv_settings { + h6 { margin-left:40px; } + } } .project_clone_panel { @@ -50,3 +79,14 @@ border: 1px solid #BBB; } } + +.save-project-loader { + img { + margin-top:50px; + margin-bottom:50px; + } + h3 { + @extend .page_title; + } + +} diff --git a/app/contexts/merge_requests_load.rb b/app/contexts/merge_requests_load.rb index 6778db3bce5..e2f68e3805b 100644 --- a/app/contexts/merge_requests_load.rb +++ b/app/contexts/merge_requests_load.rb @@ -1,13 +1,13 @@ class MergeRequestsLoad < BaseContext def execute - type = params[:f].to_i + type = params[:f] 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) + when 'all' then merge_requests + when 'closed' then merge_requests.closed + when 'assigned-to-me' then merge_requests.opened.assigned(current_user) else merge_requests.opened end.page(params[:page]).per(20) diff --git a/app/controllers/issues_controller.rb b/app/controllers/issues_controller.rb index 889a7d98033..a47b38435f2 100644 --- a/app/controllers/issues_controller.rb +++ b/app/controllers/issues_controller.rb @@ -60,7 +60,13 @@ class IssuesController < ApplicationController @issue.save respond_to do |format| - format.html { redirect_to project_issue_path(@project, @issue) } + format.html do + if @issue.valid? + redirect_to project_issue_path(@project, @issue) + else + render :new + end + end format.js end end @@ -162,10 +168,10 @@ class IssuesController < ApplicationController def issues_filter { - all: "1", - closed: "2", - to_me: "3", - open: "0" + all: "all", + closed: "closed", + to_me: "assigned-to-me", + open: "open" } end end diff --git a/app/controllers/milestones_controller.rb b/app/controllers/milestones_controller.rb index 7acb3781fbb..10f089f138b 100644 --- a/app/controllers/milestones_controller.rb +++ b/app/controllers/milestones_controller.rb @@ -17,8 +17,8 @@ class MilestonesController < ApplicationController respond_to :html def index - @milestones = case params[:f].to_i - when 1; @project.milestones + @milestones = case params[:f] + when 'all'; @project.milestones else @project.milestones.active end diff --git a/app/decorators/commit_decorator.rb b/app/decorators/commit_decorator.rb index cc8fa97587b..074176ae802 100644 --- a/app/decorators/commit_decorator.rb +++ b/app/decorators/commit_decorator.rb @@ -1,6 +1,15 @@ class CommitDecorator < ApplicationDecorator decorates :commit + # Returns a string describing the commit for use in a link title + # + # Example + # + # "Commit: Alex Denisov - Project git clone panel" + def link_title + "Commit: #{author_name} - #{title}" + end + # Returns the commits title. # # Usually, the commit title is the first line of the commit message. diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index 38191f55571..3dafb7534c3 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -78,16 +78,16 @@ module ApplicationHelper end def show_last_push_widget?(event) - event && + event && event.last_push_to_non_root? && !event.rm_ref? && - event.project && + event.project && event.project.merge_requests_enabled end def tab_class(tab_key) active = case tab_key - + # Project Area when :wall; wall_tab? when :wiki; controller.controller_name == "wikis" @@ -126,4 +126,13 @@ module ApplicationHelper def hexdigest(string) Digest::SHA1.hexdigest string end + + def project_last_activity project + activity = project.last_activity + if activity && activity.created_at + time_ago_in_words(activity.created_at) + " ago" + else + "Never" + end + end end diff --git a/app/helpers/gitlab_markdown_helper.rb b/app/helpers/gitlab_markdown_helper.rb index b4e3b9626e0..9da695b5d23 100644 --- a/app/helpers/gitlab_markdown_helper.rb +++ b/app/helpers/gitlab_markdown_helper.rb @@ -31,7 +31,7 @@ module GitlabMarkdownHelper extractions[$1] end - text.html_safe + sanitize text.html_safe, attributes: ActionView::Base.sanitized_allowed_attributes + %w(id class ) end # Use this in places where you would normally use link_to(gfm(...), ...). diff --git a/app/mailers/notify.rb b/app/mailers/notify.rb index 1d222eb1a53..91136fee95e 100644 --- a/app/mailers/notify.rb +++ b/app/mailers/notify.rb @@ -83,6 +83,14 @@ class Notify < ActionMailer::Base subject: subject("access to project was granted")) end + def issue_status_changed_email(recipient_id, issue_id, status, updated_by_user_id) + @issue = Issue.find issue_id + @issue_status = status + @updated_by = User.find updated_by_user_id + mail(to: recipient(recipient_id), + subject: subject("changed issue ##{@issue.id}", @issue.title)) + end + private # Look up a User by their ID and return their email address diff --git a/app/models/commit.rb b/app/models/commit.rb index 5c6b4d88d96..15afedcb101 100644 --- a/app/models/commit.rb +++ b/app/models/commit.rb @@ -1,6 +1,7 @@ class Commit include ActiveModel::Conversion include Gitlab::Encode + include StaticModel extend ActiveModel::Naming attr_accessor :commit @@ -22,8 +23,7 @@ class Commit :to_patch, to: :commit - - class << self + class << self def find_or_first(repo, commit_id = nil, root_ref) commit = if commit_id repo.commit(commit_id) @@ -85,7 +85,7 @@ class Commit first = project.commit(to.try(:strip)) last = project.commit(from.try(:strip)) - result = { + result = { commits: [], diffs: [], commit: nil @@ -105,10 +105,6 @@ class Commit end end - def persisted? - false - end - def initialize(raw_commit, head = nil) @commit = raw_commit @head = head @@ -155,7 +151,7 @@ class Commit prev_commit.try :id end - def parents_count + def parents_count parents && parents.count || 0 end end diff --git a/app/models/project.rb b/app/models/project.rb index 3fe11916504..a7735a42137 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -8,7 +8,7 @@ class Project < ActiveRecord::Base # # Relations - # + # belongs_to :owner, class_name: "User" has_many :users, through: :users_projects has_many :events, dependent: :destroy @@ -25,12 +25,12 @@ class Project < ActiveRecord::Base attr_accessor :error_code - # + # # Protected attributes # attr_protected :private_flag, :owner_id - # + # # Scopes # scope :public_only, where(private_flag: false) @@ -158,7 +158,7 @@ class Project < ActiveRecord::Base end def last_activity - events.last || nil + events.order("created_at ASC").last end def last_activity_date diff --git a/app/observers/issue_observer.rb b/app/observers/issue_observer.rb index 92b0f8368cb..62fd9bf8ac9 100644 --- a/app/observers/issue_observer.rb +++ b/app/observers/issue_observer.rb @@ -9,8 +9,16 @@ class IssueObserver < ActiveRecord::Observer def after_update(issue) send_reassigned_email(issue) if issue.is_being_reassigned? - Note.create_status_change_note(issue, current_user, 'closed') if issue.is_being_closed? - Note.create_status_change_note(issue, current_user, 'reopened') if issue.is_being_reopened? + + status = nil + status = 'closed' if issue.is_being_closed? + status = 'reopened' if issue.is_being_reopened? + if status + Note.create_status_change_note(issue, current_user, status) + [issue.author, issue.assignee].compact.each do |recipient| + Notify.issue_status_changed_email(recipient.id, issue.id, status, current_user) + end + end end protected diff --git a/app/roles/static_model.rb b/app/roles/static_model.rb new file mode 100644 index 00000000000..d26c8f47501 --- /dev/null +++ b/app/roles/static_model.rb @@ -0,0 +1,35 @@ +# Provides an ActiveRecord-like interface to a model whose data is not persisted to a database. +module StaticModel + extend ActiveSupport::Concern + + module ClassMethods + # Used by ActiveRecord's polymorphic association to set object_id + def primary_key + 'id' + end + + # Used by ActiveRecord's polymorphic association to set object_type + def base_class + self + end + end + + # Used by AR for fetching attributes + # + # Pass it along if we respond to it. + def [](key) + send(key) if respond_to?(key) + end + + def to_param + id + end + + def persisted? + false + end + + def destroyed? + false + end +end diff --git a/app/views/admin/projects/_form.html.haml b/app/views/admin/projects/_form.html.haml index 7cebddf2890..87d212e5710 100644 --- a/app/views/admin/projects/_form.html.haml +++ b/app/views/admin/projects/_form.html.haml @@ -10,19 +10,17 @@ Project name is .input = f.text_field :name, placeholder: "Example Project", class: "xxlarge" - = f.submit project.new_record? ? 'Create project' : 'Save Project', class: "btn primary" %hr - .alert.alert-info - %h5 Advanced settings: + .adv_settings + %h6 Advanced settings: .clearfix = f.label :path do - Git Clone + Path .input .input-prepend - %span.add-on= Gitlab.config.ssh_path - = f.text_field :path, placeholder: "example_project", disabled: !!project.id - %span.add-on= ".git" + %strong + = text_field_tag :ppath, @admin_project.path_to_repo, class: "xlarge", disabled: true .clearfix = f.label :code do URL @@ -42,8 +40,9 @@ .input= f.select(:default_branch, project.heads.map(&:name), {}, style: "width:210px;") - unless project.new_record? - .alert.alert-info - %h5 Features: + %hr + .adv_settings + %h6 Features: .clearfix = f.label :issues_enabled, "Issues" @@ -63,7 +62,8 @@ - unless project.new_record? .actions - = f.submit 'Save Project', class: "btn primary" + = f.submit 'Save Project', class: "btn save-btn" + = link_to 'Cancel', admin_projects_path, class: "btn cancel-btn" diff --git a/app/views/admin/projects/_new_form.html.haml b/app/views/admin/projects/_new_form.html.haml new file mode 100644 index 00000000000..d793b6f3aff --- /dev/null +++ b/app/views/admin/projects/_new_form.html.haml @@ -0,0 +1,29 @@ += form_for [:admin, @admin_project] do |f| + - if @admin_project.errors.any? + .alert-message.block-message.error + %span= @admin_project.errors.full_messages.first + .clearfix.project_name_holder + = f.label :name do + Project name is + .input + = f.text_field :name, placeholder: "Example Project", class: "xxlarge" + = f.submit 'Create project', class: "btn primary project-submit" + + %hr + %div.adv_settings + %h6 Advanced settings: + .clearfix + = f.label :path do + Git Clone + .input + .input-prepend + %span.add-on= Gitlab.config.ssh_path + = f.text_field :path, placeholder: "example_project", disabled: !@admin_project.new_record? + %span.add-on= ".git" + .clearfix + = f.label :code do + URL + .input + .input-prepend + %span.add-on= web_app_url + = f.text_field :code, placeholder: "example" diff --git a/app/views/admin/projects/new.html.haml b/app/views/admin/projects/new.html.haml index ac6526bfa4b..933cb671142 100644 --- a/app/views/admin/projects/new.html.haml +++ b/app/views/admin/projects/new.html.haml @@ -1,3 +1,12 @@ -%h3.page_title New project -%hr -= render 'form', project: @admin_project +.project_new_holder + %h3.page_title + New Project + %hr + = render 'new_form' +%div.save-project-loader.hide + %center + = image_tag "ajax_loader.gif" + %h3 Creating project & repository. Please wait a few minutes + +:javascript + $(function(){ new Projects(); }); diff --git a/app/views/admin/resque/show.html.haml b/app/views/admin/resque/show.html.haml index 0375d94bc9d..d889a5d0817 100644 --- a/app/views/admin/resque/show.html.haml +++ b/app/views/admin/resque/show.html.haml @@ -1,2 +1,2 @@ %h3 Resque -%iframe{src: "/info/resque", width: 1168, height: 600, style: "border: none"}
\ No newline at end of file +%iframe{src: resque_url, width: 1168, height: 600, style: "border: none"} diff --git a/app/views/admin/users/_form.html.haml b/app/views/admin/users/_form.html.haml index 2014472375b..7010c2727c6 100644 --- a/app/views/admin/users/_form.html.haml +++ b/app/views/admin/users/_form.html.haml @@ -73,8 +73,8 @@ .span6 .span6 .actions - = f.submit 'Save', class: "btn primary" + = f.submit 'Save', class: "btn save-btn" - if @admin_user.new_record? - = link_to 'Cancel', admin_users_path, class: "btn" + = link_to 'Cancel', admin_users_path, class: "btn cancel-btn" - else - = link_to 'Cancel', admin_user_path(@admin_user), class: "btn" + = link_to 'Cancel', admin_user_path(@admin_user), class: "btn cancel-btn" diff --git a/app/views/commits/_commit.html.haml b/app/views/commits/_commit.html.haml index 686a4337b3a..61371d14c5f 100644 --- a/app/views/commits/_commit.html.haml +++ b/app/views/commits/_commit.html.haml @@ -4,8 +4,8 @@ %strong= link_to "Browse Code »", tree_project_ref_path(@project, commit.id), class: "right" %p = link_to commit.short_id(8), project_commit_path(@project, id: commit.id), class: "commit_short_id" - %strong.cgray= commit.author_name - – + %strong.commit-author-name= commit.author_name + %span.dash – = image_tag gravatar_icon(commit.author_email), class: "avatar", width: 16 = link_to_gfm truncate(commit.title, length: 50), project_commit_path(@project, id: commit.id), class: "row_title" diff --git a/app/views/commits/compare.html.haml b/app/views/commits/compare.html.haml index be915cd1038..7dab1f5c0fa 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 btn-primary" + = submit_tag "Compare", class: "btn primary" - unless @commits.empty? diff --git a/app/views/commits/show.html.haml b/app/views/commits/show.html.haml index 9a483aa2a9a..e01f8ea5878 100644 --- a/app/views/commits/show.html.haml +++ b/app/views/commits/show.html.haml @@ -5,12 +5,6 @@ :javascript - $(document).ready(function(){ - $(".line_note_link, .line_note_reply_link").live("click", function(e) { - var form = $(".per_line_form"); - $(this).parent().parent().after(form); - form.find("#note_line_code").val($(this).attr("line_code")); - form.show(); - return false; - }); + $(function(){ + PerLineNotes.init(); }); diff --git a/app/views/dashboard/index.html.haml b/app/views/dashboard/index.html.haml index ba7d019cb63..e13640fb835 100644 --- a/app/views/dashboard/index.html.haml +++ b/app/views/dashboard/index.html.haml @@ -19,13 +19,16 @@ = link_to new_project_path, class: "btn very_small info" do %i.icon-plus New Project - - @projects.each do |project| - = link_to project_path(project), class: dom_class(project) do - %h4 - %span.ico.project - = truncate(project.name, length: 25) - %span.right - → + %ul.unstyled + - @projects.each do |project| + %li.wll + = link_to project_path(project), class: dom_class(project) do + %strong.project_name= truncate(project.name, length: 25) + %span.arrow + → + %span.last_activity + %strong Last activity: + %span= project_last_activity(project) .bottom= paginate @projects, theme: "gitlab" %hr diff --git a/app/views/events/_event_last_push.html.haml b/app/views/events/_event_last_push.html.haml index 4ef927495ae..66e149365f7 100644 --- a/app/views/events/_event_last_push.html.haml +++ b/app/views/events/_event_last_push.html.haml @@ -9,5 +9,5 @@ at %strong= link_to event.project.name, event.project - = link_to new_mr_path_from_push_event(event), title: "New Merge Request", class: "btn very_small primary" do + = link_to new_mr_path_from_push_event(event), title: "New Merge Request", class: "btn very_small" do Create Merge Request diff --git a/app/views/help/api.html.haml b/app/views/help/api.html.haml index 4f7af193741..d7d7aed4232 100644 --- a/app/views/help/api.html.haml +++ b/app/views/help/api.html.haml @@ -63,3 +63,13 @@ .file_content.wiki = preserve do = markdown File.read(Rails.root.join("doc", "api", "issues.md")) + +%br + +.file_holder#milestones + .file_title + %i.icon-file + Milestones + .file_content.wiki + = preserve do + = markdown File.read(Rails.root.join("doc", "api", "milestones.md")) diff --git a/app/views/issues/_form.html.haml b/app/views/issues/_form.html.haml index 65bf605a038..db7920b993d 100644 --- a/app/views/issues/_form.html.haml +++ b/app/views/issues/_form.html.haml @@ -43,14 +43,15 @@ .actions - if @issue.new_record? - = f.submit 'Submit new issue', class: "primary btn" + = f.submit 'Submit new issue', class: "btn save-btn" -else - = f.submit 'Save changes', class: "primary btn" + = f.submit 'Save changes', class: "save-btn btn" + - cancel_class = 'btn cancel-btn' - if request.xhr? - = link_to "Cancel", "#back", onclick: "backToIssues();", class: "btn" + = link_to "Cancel", "#back", onclick: "backToIssues();", class: cancel_class - else - if @issue.new_record? - = link_to "Cancel", project_issues_path(@project), class: "btn" + = link_to "Cancel", project_issues_path(@project), class: cancel_class - else - = link_to "Cancel", project_issue_path(@project, @issue), class: "btn" + = link_to "Cancel", project_issue_path(@project, @issue), class: cancel_class diff --git a/app/views/issues/index.html.haml b/app/views/issues/index.html.haml index a6836fd4fd1..010b8856d65 100644 --- a/app/views/issues/index.html.haml +++ b/app/views/issues/index.html.haml @@ -6,7 +6,7 @@ .right .span5 - if can? current_user, :write_issue, @project - = link_to new_project_issue_path(@project), class: "right btn small", title: "New Issue", remote: true do + = link_to new_project_issue_path(@project), class: "right btn", title: "New Issue", remote: true do %i.icon-plus New Issue = form_tag search_project_issues_path(@project), method: :get, remote: true, id: "issue_search_form", class: :right do diff --git a/app/views/keys/_form.html.haml b/app/views/keys/_form.html.haml index 9c6e229bf49..26700803e61 100644 --- a/app/views/keys/_form.html.haml +++ b/app/views/keys/_form.html.haml @@ -19,6 +19,6 @@ .actions - = f.submit 'Save', class: "primary btn" - = link_to "Cancel", keys_path, class: "btn" + = f.submit 'Save', class: "btn save-btn" + = link_to "Cancel", keys_path, class: "btn cancel-btn" diff --git a/app/views/keys/index.html.haml b/app/views/keys/index.html.haml index 04e9e4cbc33..9b5663ed93e 100644 --- a/app/views/keys/index.html.haml +++ b/app/views/keys/index.html.haml @@ -1,6 +1,6 @@ %h3.page_title SSH Keys - = link_to "Add new", new_key_path, class: "btn small right" + = link_to "Add new", new_key_path, class: "btn right" %hr %p.slead diff --git a/app/views/merge_requests/_form.html.haml b/app/views/merge_requests/_form.html.haml index b6c12397d6c..b554c051964 100644 --- a/app/views/merge_requests/_form.html.haml +++ b/app/views/merge_requests/_form.html.haml @@ -9,7 +9,7 @@ %br .row - .span6 + .span5 .mr_branch_box %h5 From (Head Branch) .body @@ -17,10 +17,11 @@ = f.label :source_branch, "From", class: "control-label" .controls = f.select(:source_branch, @project.heads.map(&:name), { include_blank: "Select branch" }, style: "width:250px") - .bottom_commit - .mr_source_commit + .mr_source_commit - .span6 + .span2 + %center= image_tag "merge.png", class: 'mr_direction_tip' + .span5 .mr_branch_box %h5 To (Base Branch) .body @@ -28,8 +29,7 @@ = f.label :target_branch, "To", class: "control-label" .controls = f.select(:target_branch, @project.heads.map(&:name), { include_blank: "Select branch" }, style: "width:250px") - .bottom_commit - .mr_target_commit + .mr_target_commit %h4.cdark 2. Fill info @@ -48,18 +48,19 @@ .control-group .form-actions - = f.submit 'Save', class: "btn-primary btn" + = f.submit 'Save', class: "btn save-btn" - if @merge_request.new_record? - = link_to project_merge_requests_path(@project), class: "btn" do + = link_to project_merge_requests_path(@project), class: "btn cancel-btn" do Cancel - else - = link_to project_merge_request_path(@project, @merge_request), class: "btn" do + = link_to project_merge_request_path(@project, @merge_request), class: "btn cancel-btn" do Cancel :javascript $(function(){ + disableButtonIfEmtpyField("#merge_request_title", ".save-btn"); $('select#merge_request_assignee_id').chosen(); $('select#merge_request_source_branch').chosen(); $('select#merge_request_target_branch').chosen(); diff --git a/app/views/merge_requests/index.html.haml b/app/views/merge_requests/index.html.haml index 4ad6e5c18ce..bbf35dc72d2 100644 --- a/app/views/merge_requests/index.html.haml +++ b/app/views/merge_requests/index.html.haml @@ -1,7 +1,7 @@ %h3.page_title Merge Requests - if can? current_user, :write_issue, @project - = link_to new_project_merge_request_path(@project), class: "right btn small", title: "New Merge Request" do + = link_to new_project_merge_request_path(@project), class: "right btn", title: "New Merge Request" do New Merge Request %br @@ -10,17 +10,17 @@ .ui-box .title %ul.nav.nav-pills - %li{class: ("active" if (params[:f] == "0" || !params[:f]))} - = link_to project_merge_requests_path(@project, f: 0) do + %li{class: ("active" if (params[:f] == 'open' || !params[:f]))} + = link_to project_merge_requests_path(@project, f: 'open') do Open - %li{class: ("active" if params[:f] == "2")} - = link_to project_merge_requests_path(@project, f: 2) do + %li{class: ("active" if params[:f] == "closed")} + = link_to project_merge_requests_path(@project, f: "closed") do Closed - %li{class: ("active" if params[:f] == "3")} - = link_to project_merge_requests_path(@project, f: 3) do + %li{class: ("active" if params[:f] == 'assigned-to-me')} + = link_to project_merge_requests_path(@project, f: 'assigned-to-me') do To Me - %li{class: ("active" if params[:f] == "1")} - = link_to project_merge_requests_path(@project, f: 1) do + %li{class: ("active" if params[:f] == 'all')} + = link_to project_merge_requests_path(@project, f: 'all') do All %ul.unstyled diff --git a/app/views/merge_requests/show/_mr_title.html.haml b/app/views/merge_requests/show/_mr_title.html.haml index 31fa0779fd2..3ae1050d169 100644 --- a/app/views/merge_requests/show/_mr_title.html.haml +++ b/app/views/merge_requests/show/_mr_title.html.haml @@ -1,9 +1,9 @@ %h3.page_title = "Merge Request ##{@merge_request.id}:" - %span.pretty_label.branch= @merge_request.source_branch + %span.label_branch= @merge_request.source_branch → - %span.pretty_label.branch= @merge_request.target_branch + %span.label_branch= @merge_request.target_branch %span.right - if @merge_request.merged? diff --git a/app/views/milestones/_form.html.haml b/app/views/milestones/_form.html.haml index 49200c6705f..41cbd6abcad 100644 --- a/app/views/milestones/_form.html.haml +++ b/app/views/milestones/_form.html.haml @@ -32,20 +32,16 @@ .form-actions - if @milestone.new_record? - = f.submit 'Create milestone', class: "primary btn" + = f.submit 'Create milestone', class: "save-btn btn" + = link_to "Cancel", project_milestones_path(@project), class: "btn cancel-btn" -else - = f.submit 'Save changes', class: "primary btn" + = f.submit 'Save changes', class: "save-btn btn" + = link_to "Cancel", project_milestone_path(@project, @milestone), class: "btn cancel-btn" - - if request.xhr? - = link_to "Cancel", "#back", onclick: "backToIssues();", class: "btn" - - else - - if @milestone.new_record? - = link_to "Cancel", project_milestones_path(@project), class: "btn" - - else - = link_to "Cancel", project_milestone_path(@project, @milestone), class: "btn" :javascript $(function() { + disableButtonIfEmtpyField("#milestone_title", ".save-btn"); $( ".datepicker" ).datepicker({ dateFormat: "yy-mm-dd", onSelect: function(dateText, inst) { $("#milestone_due_date").val(dateText) } diff --git a/app/views/milestones/index.html.haml b/app/views/milestones/index.html.haml index ecb008dc144..c5333b08fdc 100644 --- a/app/views/milestones/index.html.haml +++ b/app/views/milestones/index.html.haml @@ -8,11 +8,11 @@ %div.ui-box .title %ul.nav.nav-pills - %li{class: ("active" if (params[:f] == "0" || !params[:f]))} - = link_to project_milestones_path(@project, f: 0) do + %li{class: ("active" if (params[:f] == "active" || !params[:f]))} + = link_to project_milestones_path(@project, f: "active") do Active - %li{class: ("active" if params[:f] == "1")} - = link_to project_milestones_path(@project, f: 1) do + %li{class: ("active" if params[:f] == "all")} + = link_to project_milestones_path(@project, f: "all") do All %ul.unstyled diff --git a/app/views/notes/_create_common.js.haml b/app/views/notes/_create_common.js.haml index e9538902754..e80eccb1b4c 100644 --- a/app/views/notes/_create_common.js.haml +++ b/app/views/notes/_create_common.js.haml @@ -1,11 +1,12 @@ - if note.valid? :plain - $("#new_note .error").remove(); - $('#new_note textarea').val(""); - $('#preview-link').text('Preview'); - $('#preview-note').hide(); $('#note_note').show(); + $(".note-form-holder .error").remove(); + $('.note-form-holder textarea').val(""); + $('.note-form-holder #preview-link').text('Preview'); + $('.note-form-holder #preview-note').hide(); + $('.note-form-holder').show(); NoteList.prepend(#{note.id}, "#{escape_javascript(render partial: "notes/show", locals: {note: note})}"); - else :plain - $("#new_note").replaceWith("#{escape_javascript(render('form'))}"); + $(".note-form-holder").replaceWith("#{escape_javascript(render('form'))}"); diff --git a/app/views/notes/_create_line.js.haml b/app/views/notes/_create_line.js.haml index 13809bec1b9..662909f7967 100644 --- a/app/views/notes/_create_line.js.haml +++ b/app/views/notes/_create_line.js.haml @@ -1,7 +1,7 @@ - if note.valid? :plain $(".per_line_form").hide(); - $('#new_note textarea').val(""); + $('.line-note-form-holder textarea').val(""); $("a.line_note_reply_link[line_code='#{note.line_code}']").closest("tr").remove(); var trEl = $(".#{note.line_code}").parent(); trEl.after("#{escape_javascript(render partial: "notes/per_line_show", locals: {note: note})}"); diff --git a/app/views/notes/_form.html.haml b/app/views/notes/_form.html.haml index 326f1add485..7211a0ae471 100644 --- a/app/views/notes/_form.html.haml +++ b/app/views/notes/_form.html.haml @@ -1,37 +1,39 @@ -= form_for [@project, @note], remote: "true", multipart: true do |f| - %h3.page_title Leave a comment - -if @note.errors.any? - .alert-message.block-message.error - - @note.errors.full_messages.each do |msg| - %div= msg +.note-form-holder + = form_for [@project, @note], remote: "true", multipart: true do |f| + %h3.page_title Leave a comment + -if @note.errors.any? + .alert-message.block-message.error + - @note.errors.full_messages.each do |msg| + %div= msg - = f.hidden_field :noteable_id - = f.hidden_field :noteable_type - = f.text_area :note, size: 255 - #preview-note.well.hide - .hint - = link_to 'Preview', preview_project_notes_path(@project), id: 'preview-link' - .right Comments are parsed with #{link_to "Gitlab Flavored Markdown", help_markdown_path, target: '_blank'}. + = f.hidden_field :noteable_id + = f.hidden_field :noteable_type + = f.text_area :note, size: 255, class: 'note-text' + #preview-note.preview_note.hide + .hint + .right Comments are parsed with #{link_to "Gitlab Flavored Markdown", help_markdown_path, target: '_blank'}. + .clearfix - .row.note_advanced_opts.hide - .span2 - = f.submit 'Add Comment', class: "btn primary submit_note", id: "submit_note" - .span4.notify_opts - %h6.left Notify via email: - = label_tag :notify do - = check_box_tag :notify, 1, @note.noteable_type != "Commit" - %span Project team + .row.note_advanced_opts.hide + .span3 + = f.submit 'Add Comment', class: "btn success submit_note grouped", id: "submit_note" + = link_to 'Preview', preview_project_notes_path(@project), class: 'btn grouped', id: 'preview-link' + .span4.notify_opts + %h6.left Notify via email: + = label_tag :notify do + = check_box_tag :notify, 1, @note.noteable_type != "Commit" + %span Project team - - if @note.notify_only_author?(current_user) - = label_tag :notify_author do - = check_box_tag :notify_author, 1 , @note.noteable_type == "Commit" - %span Commit author - .span6.attachments - %h6.left Attachment: - %span.file_name File name... - - .input.input_file - %a.file_upload.btn.small Upload File - = f.file_field :attachment, class: "input-file" - %span.hint Any file less than 10 MB + - if @note.notify_only_author?(current_user) + = label_tag :notify_author do + = check_box_tag :notify_author, 1 , @note.noteable_type == "Commit" + %span Commit author + .span5.attachments + %h6.left Attachment: + %span.file_name File name... + + .input.input_file + %a.file_upload.btn.small Upload File + = f.file_field :attachment, class: "input-file" + %span.hint Any file less than 10 MB diff --git a/app/views/notes/_per_line_form.html.haml b/app/views/notes/_per_line_form.html.haml index afb0b30dca3..8e31b59e9f0 100644 --- a/app/views/notes/_per_line_form.html.haml +++ b/app/views/notes/_per_line_form.html.haml @@ -1,33 +1,34 @@ %table{style: "display:none;"} %tr.per_line_form %td{colspan: 3 } - = form_for [@project, @note], remote: "true", multipart: true do |f| - %h3.page_title Leave a note - %div.span10 - -if @note.errors.any? - .alert-message.block-message.error - - @note.errors.full_messages.each do |msg| - %div= msg + .line-note-form-holder + = form_for [@project, @note], remote: "true", multipart: true do |f| + %h3.page_title Leave a note + %div.span10 + -if @note.errors.any? + .alert-message.block-message.error + - @note.errors.full_messages.each do |msg| + %div= msg - = f.hidden_field :noteable_id - = f.hidden_field :noteable_type - = f.hidden_field :line_code - = f.text_area :note, size: 255 - .note_actions - .buttons - = f.submit 'Add note', class: "btn primary submit_note", id: "submit_note" - = link_to "Cancel", "#", class: "btn hide-button" - .options - %h6.left Notify via email: - .labels - = label_tag :notify do - = check_box_tag :notify, 1, @note.noteable_type != "Commit" - %span Project team + = f.hidden_field :noteable_id + = f.hidden_field :noteable_type + = f.hidden_field :line_code + = f.text_area :note, size: 255, class: 'line-note-text' + .note_actions + .buttons + = f.submit 'Add note', class: "btn save-btn submit_note submit_inline_note", id: "submit_note" + = link_to "Cancel", "#", class: "btn hide-button" + .options + %h6.left Notify via email: + .labels + = label_tag :notify do + = check_box_tag :notify, 1, @note.noteable_type != "Commit" + %span Project team - - if @note.notify_only_author?(current_user) - = label_tag :notify_author do - = check_box_tag :notify_author, 1 , @note.noteable_type == "Commit" - %span Commit author + - if @note.notify_only_author?(current_user) + = label_tag :notify_author do + = check_box_tag :notify_author, 1 , @note.noteable_type == "Commit" + %span Commit author :javascript $(function(){ diff --git a/app/views/notify/issue_status_changed_email.html.haml b/app/views/notify/issue_status_changed_email.html.haml new file mode 100644 index 00000000000..59130f79d6c --- /dev/null +++ b/app/views/notify/issue_status_changed_email.html.haml @@ -0,0 +1,16 @@ +%td.content{align: "left", style: "font-family: Helvetica, Arial, sans-serif; padding: 20px 0 0;", valign: "top", width: "600"} + %table{border: "0", cellpadding: "0", cellspacing: "0", style: "color: #717171; font: normal 11px Helvetica, Arial, sans-serif; margin: 0; padding: 0;", width: "600"} + %tr + %td{style: "font-size: 1px; line-height: 1px;", width: "21"} + %td{align: "left", style: "padding: 20px 0 0;"} + %h2{style: "color:#646464; font-weight: bold; margin: 0; padding: 0; line-height: 26px; font-size: 18px; font-family: Helvetica, Arial, sans-serif; "} + = "Issue was #{@issue_status} by #{@updated_by.name}" + %td{style: "font-size: 1px; line-height: 1px;", width: "21"} + %tr + %td{style: "font-size: 1px; line-height: 1px;", width: "21"} + %td{align: "left", style: "padding: 20px 0 0;"} + %h2{style: "color:#646464 !important; font-weight: bold; margin: 0; padding: 0; line-height: 26px; font-size: 18px; font-family: Helvetica, Arial, sans-serif; "} + = "Issue ##{@issue.id}" + = link_to_gfm truncate(@issue.title, length: 45), project_issue_url(@issue.project, @issue), title: @issue.title + %br + diff --git a/app/views/profile/password.html.haml b/app/views/profile/password.html.haml index 257dacb1ad3..d0aee7ac6b3 100644 --- a/app/views/profile/password.html.haml +++ b/app/views/profile/password.html.haml @@ -16,4 +16,4 @@ = f.label :password_confirmation .input= f.password_field :password_confirmation .actions - = f.submit 'Save', class: "btn primary" + = f.submit 'Save', class: "btn save-btn" diff --git a/app/views/profile/show.html.haml b/app/views/profile/show.html.haml index a7b6a18ad2d..22e840a089f 100644 --- a/app/views/profile/show.html.haml +++ b/app/views/profile/show.html.haml @@ -67,4 +67,4 @@ = link_to "Add Public Key", new_key_path, class: "btn small right" .form-actions - = f.submit 'Save', class: "btn-primary btn" + = f.submit 'Save', class: "btn save-btn" diff --git a/app/views/projects/_clone_panel.html.haml b/app/views/projects/_clone_panel.html.haml new file mode 100644 index 00000000000..839a98a0d79 --- /dev/null +++ b/app/views/projects/_clone_panel.html.haml @@ -0,0 +1,21 @@ +.project_clone_panel + .row + .span7 + .form-horizontal + .input-prepend.project_clone_holder + = link_to "SSH", "#", class: "btn small active", :"data-clone" => @project.ssh_url_to_repo + = link_to "HTTP", "#", class: "btn small", :"data-clone" => @project.http_url_to_repo + = text_field_tag :project_clone, @project.url_to_repo, class: "one_click_select span5" + .span4.right + .right + - if can? current_user, :download_code, @project + = link_to archive_project_repository_path(@project), class: "btn small grouped" do + %i.icon-download-alt + Download + - if @project.merge_requests_enabled && can?(current_user, :write_merge_request, @project) + = link_to new_project_merge_request_path(@project), title: "New Merge Request", class: "btn small grouped" do + Merge Request + - if @project.issues_enabled && can?(current_user, :write_issue, @project) + = link_to new_project_issue_path(@project), title: "New Issue", class: "btn small grouped" do + Issue + diff --git a/app/views/projects/_form.html.haml b/app/views/projects/_form.html.haml index ce66b2cf930..8bdeda1cafe 100644 --- a/app/views/projects/_form.html.haml +++ b/app/views/projects/_form.html.haml @@ -10,9 +10,9 @@ .input = f.text_field :name, placeholder: "Example Project", class: "xxlarge" - %h5.page_title - .alert.alert-info - %h5 Advanced settings: + %hr + .adv_settings + %h6 Advanced settings: .clearfix = f.label :path do Path @@ -34,8 +34,9 @@ .input= f.select(:default_branch, @project.heads.map(&:name), {}, style: "width:210px;") - unless @project.new_record? - .alert.alert-info - %h5 Features: + %hr + .adv_settings + %h6 Features: .clearfix = f.label :issues_enabled, "Issues" @@ -56,7 +57,7 @@ %br .actions - = f.submit 'Save', class: "btn primary" + = f.submit 'Save', class: "btn save-btn" = link_to 'Cancel', @project, class: "btn" - unless @project.new_record? .right diff --git a/app/views/projects/_new_form.html.haml b/app/views/projects/_new_form.html.haml index 5104df83a2c..e6d5e93fca7 100644 --- a/app/views/projects/_new_form.html.haml +++ b/app/views/projects/_new_form.html.haml @@ -7,11 +7,11 @@ Project name is .input = f.text_field :name, placeholder: "Example Project", class: "xxlarge" - = f.submit 'Create project', class: "btn primary" + = f.submit 'Create project', class: "btn primary project-submit" %hr - .alert.alert-info - %h5 Advanced settings: + %div.adv_settings + %h6 Advanced settings: .clearfix = f.label :path do Git Clone diff --git a/app/views/projects/empty.html.haml b/app/views/projects/empty.html.haml index 907d5ef4666..d408c0a64ae 100644 --- a/app/views/projects/empty.html.haml +++ b/app/views/projects/empty.html.haml @@ -1,4 +1,12 @@ = render 'shared/no_ssh' +.project_clone_panel + .row + .span7 + .form-horizontal + .input-prepend.project_clone_holder + = link_to "SSH", "#", class: "btn small active", :"data-clone" => @project.ssh_url_to_repo + = link_to "HTTP", "#", class: "btn small", :"data-clone" => @project.http_url_to_repo + = text_field_tag :project_clone, @project.url_to_repo, class: "one_click_select span5" %div.git-empty %h4 Git global setup: %pre.dark @@ -28,3 +36,16 @@ - if can? current_user, :admin_project, @project .prepend-top-20 = link_to 'Remove project', @project, confirm: 'Are you sure?', method: :delete, class: "btn danger right" + + + +:javascript + $(function(){ + var link_sel = ".project_clone_holder a"; + $(link_sel).bind("click", function() { + $(link_sel).removeClass("active"); + $(this).addClass("active"); + $("#project_clone").val($(this).attr("data-clone")); + }) + }) + diff --git a/app/views/projects/new.html.haml b/app/views/projects/new.html.haml index 703e558ae41..933cb671142 100644 --- a/app/views/projects/new.html.haml +++ b/app/views/projects/new.html.haml @@ -3,10 +3,10 @@ New Project %hr = render 'new_form' -%div.ajax_loader.hide +%div.save-project-loader.hide %center - %div.padded= image_tag "ajax_loader.gif" - %h3.prepend-top Creating project & repository. Please wait a few minutes + = image_tag "ajax_loader.gif" + %h3 Creating project & repository. Please wait a few minutes :javascript $(function(){ new Projects(); }); diff --git a/app/views/projects/show.html.haml b/app/views/projects/show.html.haml index 628dc53d01b..77a0ef1ac4d 100644 --- a/app/views/projects/show.html.haml +++ b/app/views/projects/show.html.haml @@ -1,33 +1,12 @@ = render "project_head" - -.project_clone_panel - .row - .span7 - .form-horizontal - .input-prepend.project_clone_holder - = link_to "SSH", "#", class: "btn small active", :"data-clone" => @project.ssh_url_to_repo - = link_to "HTTP", "#", class: "btn small", :"data-clone" => @project.http_url_to_repo - = text_field_tag :project_clone, @project.url_to_repo, class: "one_click_select span5" - .span4.right - .right - - if can? current_user, :download_code, @project - = link_to archive_project_repository_path(@project), class: "btn small grouped" do - %i.icon-download-alt - Download - - if @project.merge_requests_enabled && can?(current_user, :write_merge_request, @project) - = link_to new_project_merge_request_path(@project), title: "New Merge Request", class: "btn small grouped" do - Merge Request - - if @project.issues_enabled && can?(current_user, :write_issue, @project) - = link_to new_project_issue_path(@project), title: "New Issue", class: "btn small grouped" do - Issue - += render 'clone_panel' = render "events/event_last_push", event: @last_push .content_list= render @events -:javascript +:javascript $(function(){ var link_sel = ".project_clone_holder a"; - $(link_sel).bind("click", function() { + $(link_sel).bind("click", function() { $(link_sel).removeClass("active"); $(this).addClass("active"); $("#project_clone").val($(this).attr("data-clone")); diff --git a/app/views/refs/_tree_item.html.haml b/app/views/refs/_tree_item.html.haml index 2e6bbf6221b..d4c4ee8de49 100644 --- a/app/views/refs/_tree_item.html.haml +++ b/app/views/refs/_tree_item.html.haml @@ -2,7 +2,7 @@ %tr{ class: "tree-item #{tree_hex_class(content)}", url: tree_file_project_ref_path(@project, @ref, file) } %td.tree-item-file-name = tree_icon(content) - = link_to truncate(content.name, length: 40), tree_file_project_ref_path(@project, @ref || @commit.id, file), remote: :true + %strong= link_to truncate(content.name, length: 40), tree_file_project_ref_path(@project, @ref || @commit.id, file), remote: :true %td.tree_time_ago.cgray - if index == 1 %span.log_loading diff --git a/app/views/search/show.html.haml b/app/views/search/show.html.haml index 9a0b4789588..d37ef670318 100644 --- a/app/views/search/show.html.haml +++ b/app/views/search/show.html.haml @@ -4,7 +4,7 @@ %strong Looking for .input = text_field_tag :search, params[:search], placeholder: "issue 143", class: "input-xxlarge", id: "dashboard_search" - = submit_tag 'Search', class: "btn btn-primary" + = submit_tag 'Search', class: "btn primary" - if params[:search].present? %br %h3 diff --git a/app/views/wikis/_form.html.haml b/app/views/wikis/_form.html.haml index 305607d4118..12b57e032a4 100644 --- a/app/views/wikis/_form.html.haml +++ b/app/views/wikis/_form.html.haml @@ -23,5 +23,5 @@ = f.label :content .input= f.text_area :content, class: 'span8' .actions - = f.submit 'Save', class: "primary btn" - = link_to "Cancel", project_wiki_path(@project, :index), class: "btn" + = f.submit 'Save', class: "save-btn btn" + = link_to "Cancel", project_wiki_path(@project, :index), class: "btn cancel-btn" diff --git a/config/routes.rb b/config/routes.rb index f895478fb12..51e65721880 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -10,7 +10,7 @@ Gitlab::Application.routes.draw do # Optionally, enable Resque here require 'resque/server' - mount Resque::Server.new, at: '/info/resque' + mount Resque::Server.new, at: '/info/resque', as: 'resque' # Enable Grack support mount Grack::Bundle.new({ diff --git a/doc/api/README.md b/doc/api/README.md index d32573aaa6b..53b4983ef47 100644 --- a/doc/api/README.md +++ b/doc/api/README.md @@ -29,3 +29,4 @@ The API uses JSON to serialize data. You don't need to specify `.json` at the en + [Projects](https://github.com/gitlabhq/gitlabhq/blob/master/doc/api/projects.md) + [Snippets](https://github.com/gitlabhq/gitlabhq/blob/master/doc/api/snippets.md) + [Issues](https://github.com/gitlabhq/gitlabhq/blob/master/doc/api/issues.md) ++ [Milestones](https://github.com/gitlabhq/gitlabhq/blob/master/doc/api/milestones.md) diff --git a/doc/api/milestones.md b/doc/api/milestones.md new file mode 100644 index 00000000000..f68d8eb7d58 --- /dev/null +++ b/doc/api/milestones.md @@ -0,0 +1,57 @@ +## List project milestones + +Get a list of project milestones. + +``` +GET /projects/:id/milestones +``` + +Parameters: + ++ `id` (required) - The ID or code name of a project + +## Single milestone + +Get a single project milestone. + +``` +GET /projects/:id/milestones/:milestone_id +``` + +Parameters: + ++ `id` (required) - The ID or code name of a project ++ `milestone_id` (required) - The ID of a project milestone + +## New milestone + +Create a new project milestone. + +``` +POST /projects/:id/milestones +``` + +Parameters: + ++ `id` (required) - The ID or code name of a project ++ `milestone_id` (required) - The ID of a project milestone ++ `title` (required) - The title of an milestone ++ `description` (optional) - The description of the milestone ++ `due_date` (optional) - The due date of the milestone + +## Edit milestone + +Update an existing project milestone. + +``` +PUT /projects/:id/milestones/:milestone_id +``` + +Parameters: + ++ `id` (required) - The ID or code name of a project ++ `milestone_id` (required) - The ID of a project milestone ++ `title` (optional) - The title of a milestone ++ `description` (optional) - The description of a milestone ++ `due_date` (optional) - The due date of the milestone ++ `closed` (optional) - The status of the milestone diff --git a/doc/debian_ubuntu.sh b/doc/debian_ubuntu.sh index a0b4710b5eb..5ed1e2a2f1f 100644 --- a/doc/debian_ubuntu.sh +++ b/doc/debian_ubuntu.sh @@ -3,7 +3,8 @@ sudo apt-get update sudo apt-get upgrade -sudo apt-get install -y git git-core 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 python-dev python-pip libyaml-dev postfix +sudo DEBIAN_FRONTEND='noninteractive' apt-get install -y postfix-policyd-spf-python # Install postfix without prompting. +sudo apt-get install -y git git-core 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 python-dev python-pip libyaml-dev wget http://ftp.ruby-lang.org/pub/ruby/1.9/ruby-1.9.3-p194.tar.gz tar xfvz ruby-1.9.3-p194.tar.gz diff --git a/doc/development.md b/doc/development.md new file mode 100644 index 00000000000..55be2bc32f2 --- /dev/null +++ b/doc/development.md @@ -0,0 +1,45 @@ +## Development tips: + +### Start application in development mode + +#### 1. Via foreman + + bundle exec foreman -p 3000 + +#### 2. Via gitlab cli + + ./gitlab start + +#### 3. Manually + + bundle exec rails s + bundle exec rake environment resque:work QUEUE=* VVERBOSE=1 + + +### Run tests: + +#### 1. Packages + + # ubuntu + sudo apt-get install libqt4-dev libqtwebkit-dev + sudo apt-get install xvfb + + # Mac + brew install qt + brew install xvfb + +#### 2. DB & seeds + + bundle exec rake db:setup RAILS_ENV=test + bundle exec rake db:seed_fu RAILS_ENV=test + +### 3. Run Tests + + # All in one + bundle exec gitlab:test + + # Rspec + bundle exec rake spec + + # Cucumber + bundle exec rake cucumber diff --git a/doc/installation.md b/doc/installation.md index df106bc10a6..efed21f0b28 100644 --- a/doc/installation.md +++ b/doc/installation.md @@ -1,4 +1,4 @@ -## Platform requirements: +## Platform requirements: **The project is designed for the Linux operating system.** @@ -22,7 +22,7 @@ You might have some luck using these, but no guarantees: Gitlab does **not** run on Windows and we have no plans of making Gitlab compatible. -## This installation guide created for Debian/Ubuntu and properly tested. +## This installation guide created for Debian/Ubuntu and properly tested. The installation consists of 6 steps: @@ -43,13 +43,13 @@ Also read the [Read this before you submit an issue](https://github.com/gitlabhq > - - - > First 3 steps can be easily skipped with simply install script: -> -> # Install curl and sudo +> +> # Install curl and sudo > apt-get install curl sudo -> +> > # 3 steps in 1 command :) > curl https://raw.github.com/gitlabhq/gitlabhq/master/doc/debian_ubuntu.sh | sh -> +> > Now you can go to step 4" > - - - @@ -61,7 +61,7 @@ Also read the [Read this before you submit an issue](https://github.com/gitlabhq sudo apt-get upgrade 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 postfix - + # If you want to use MySQL: sudo apt-get install -y mysql-server mysql-client libmysqlclient-dev @@ -77,7 +77,7 @@ Also read the [Read this before you submit an issue](https://github.com/gitlabhq # 3. Install gitolite Create user for git: - + sudo adduser \ --system \ --shell /bin/sh \ @@ -90,7 +90,7 @@ Create user for git: Create user for gitlab: # ubuntu/debian - sudo adduser --disabled-login --gecos 'gitlab system' gitlab + sudo adduser --disabled-login --gecos 'gitlab system' gitlab Add your user to git group: @@ -103,7 +103,7 @@ Generate key: Get gitolite source code: cd /home/git - sudo -H -u git git clone git://github.com/gitlabhq/gitolite /home/git/gitolite + sudo -H -u git git clone git://github.com/gitlabhq/gitolite /home/git/gitolite Setup: @@ -114,20 +114,20 @@ Setup: 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" - + Permissions: sudo chmod -R g+rwX /home/git/repositories/ sudo chown -R git:git /home/git/repositories/ #### CHECK: Logout & login again to apply git group to your user - + # clone admin repo to add localhost to known_hosts # & be sure your user has access to gitolite - sudo -u gitlab -H git clone git@localhost:gitolite-admin.git /tmp/gitolite-admin + sudo -u gitlab -H git clone git@localhost:gitolite-admin.git /tmp/gitolite-admin # if succeed you can remove it - sudo rm -rf /tmp/gitolite-admin + sudo rm -rf /tmp/gitolite-admin **IMPORTANT! If you cant clone `gitolite-admin` repository - DONT PROCEED INSTALLATION** @@ -139,7 +139,7 @@ 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 @@ -150,22 +150,22 @@ Permissions: # SQLite sudo -u gitlab cp config/database.yml.sqlite config/database.yml - # Or + # Or # Mysql # Install MySQL as directed in Step #1 - + # Login to MySQL - $ mysql -u root -p - + $ mysql -u root -p + # Create the gitlabhq production database mysql> CREATE DATABASE IF NOT EXISTS `gitlabhq_production` DEFAULT CHARACTER SET `utf8` COLLATE `utf8_unicode_ci`; - + # Create the MySQL User change $password to a real password - mysql> CREATE USER 'gitlab'@'localhost' IDENTIFIED BY '$password'; - + mysql> CREATE USER 'gitlab'@'localhost' IDENTIFIED BY '$password'; + # Grant proper permissions to the MySQL User mysql> GRANT SELECT, INSERT, UPDATE, DELETE, CREATE, DROP, INDEX, ALTER ON `gitlabhq_production`.* TO 'gitlab'@'localhost'; - + # Exit MySQL Server and copy the example config, make sure to update username/password in config/database.yml sudo -u gitlab cp config/database.yml.example config/database.yml @@ -181,7 +181,7 @@ Permissions: sudo cp ./lib/hooks/post-receive /home/git/share/gitolite/hooks/common/post-receive sudo chown git:git /home/git/share/gitolite/hooks/common/post-receive - + Checking status: sudo -u gitlab bundle exec rake gitlab:app:status RAILS_ENV=production @@ -202,13 +202,13 @@ Checking status: UMASK for .gitolite.rc is 0007? ............YES /home/git/share/gitolite/hooks/common/post-receive exists? ............YES -If you got all YES - congrats! You can go to next step. +If you got all YES - congrats! You can go to next step. # 5. Server up Application can be started with next command: - # For test purposes + # For test purposes sudo -u gitlab bundle exec rails s -e production # As daemon @@ -246,12 +246,13 @@ You can login via web using admin generated with setup: Add gitlab to nginx sites & change with your host specific settings - cp /home/gitlab/gitlab/lib/support/nginx-gitlab /etc/nginx/sites-enabled/gitlab + sudo cp /home/gitlab/gitlab/lib/support/nginx-gitlab /etc/nginx/sites-available/gitlab + sudo ln -s /etc/nginx/sites-available/gitlab /etc/nginx/sites-enabled/gitlab # Change **YOUR_SERVER_IP** and **YOUR_SERVER_FQDN** # to the IP address and fully-qualified domain name # of the host serving GitLab. - vim /etc/nginx/sites-enabled/nginx-gitlab + sudo vim /etc/nginx/sites-enabled/gitlab Restart nginx: diff --git a/gitlab b/gitlab new file mode 100755 index 00000000000..acafb3f10c4 --- /dev/null +++ b/gitlab @@ -0,0 +1,75 @@ +#!/usr/bin/env ruby + +class GitlabCli + def initialize + @path = File.dirname(__FILE__) + @command = ARGV.shift + @mode = ARGV.shift + end + + def execute + case @command + when 'start' then start + when 'stop' then stop + else + puts "-- Usage gitlab start production or gitlab stop development" + end + end + + private + + def start + case @mode + when 'production'; + system(unicorn_start_cmd) + system(resque_start_cmd) + else + system(rails_start_cmd) + system(resque_dev_start_cmd) + end + end + + def stop + case @mode + when 'production'; + system(unicorn_stop_cmd) + else + system(rails_stop_cmd) + end + system(resque_stop_cmd) + end + + def rails_start_cmd + "bundle exec rails s -d" + end + + def rails_stop_cmd + pid = File.join(@path, "tmp/pids/server.pid") + "kill -QUIT `cat #{pid}`" + end + + def unicorn_start_cmd + unicorn_conf = File.join(@path, "config/unicorn.rb") + "bundle exec unicorn_rails -c #{unicorn_conf} -E production -D" + end + + def unicorn_stop_cmd + pid = File.join(@path, "/tmp/pids/unicorn.pid") + "kill -QUIT `cat #{pid}`" + end + + def resque_dev_start_cmd + "./resque_dev.sh > /dev/null 2>&1" + end + + def resque_start_cmd + "./resque.sh > /dev/null 2>&1" + end + + def resque_stop_cmd + pid = File.join(@path, "tmp/pids/resque_worker.pid") + "kill -QUIT `cat #{pid}`" + end +end + +GitlabCli.new.execute diff --git a/lib/api.rb b/lib/api.rb index 3ff3b3836f4..be04701c25d 100644 --- a/lib/api.rb +++ b/lib/api.rb @@ -16,5 +16,6 @@ module Gitlab mount Users mount Projects mount Issues + mount Milestones end end diff --git a/lib/api/issues.rb b/lib/api/issues.rb index 2abc20ad34e..836c2818544 100644 --- a/lib/api/issues.rb +++ b/lib/api/issues.rb @@ -95,7 +95,7 @@ module Gitlab end end - # Delete a project issue + # Delete a project issue (deprecated) # # Parameters: # id (required) - The ID or code name of a project @@ -103,8 +103,7 @@ module Gitlab # Example Request: # DELETE /projects/:id/issues/:issue_id delete ":id/issues/:issue_id" do - @issue = user_project.issues.find(params[:issue_id]) - @issue.destroy + error!({'message' => 'method not allowed'}, 405) end end end diff --git a/lib/api/milestones.rb b/lib/api/milestones.rb new file mode 100644 index 00000000000..f537b8e5bf2 --- /dev/null +++ b/lib/api/milestones.rb @@ -0,0 +1,80 @@ +module Gitlab + # Milestones API + class Milestones < Grape::API + before { authenticate! } + + resource :projects do + # Get a list of project milestones + # + # Parameters: + # id (required) - The ID or code name of a project + # Example Request: + # GET /projects/:id/milestones + get ":id/milestones" do + present user_project.milestones, with: Entities::Milestone + end + + # Get a single project milestone + # + # Parameters: + # id (required) - The ID or code name of a project + # milestone_id (required) - The ID of a project milestone + # Example Request: + # GET /projects/:id/milestones/:milestone_id + get ":id/milestones/:milestone_id" do + @milestone = user_project.milestones.find(params[:milestone_id]) + present @milestone, with: Entities::Milestone + end + + # Create a new project milestone + # + # Parameters: + # id (required) - The ID or code name of the project + # title (required) - The title of the milestone + # description (optional) - The description of the milestone + # due_date (optional) - The due date of the milestone + # Example Request: + # POST /projects/:id/milestones + post ":id/milestones" do + @milestone = user_project.milestones.new( + title: params[:title], + description: params[:description], + due_date: params[:due_date] + ) + + if @milestone.save + present @milestone, with: Entities::Milestone + else + error!({'message' => '404 Not found'}, 404) + end + end + + # Update an existing project milestone + # + # Parameters: + # id (required) - The ID or code name of a project + # milestone_id (required) - The ID of a project milestone + # title (optional) - The title of a milestone + # description (optional) - The description of a milestone + # due_date (optional) - The due date of a milestone + # closed (optional) - The status of the milestone + # Example Request: + # PUT /projects/:id/milestones/:milestone_id + put ":id/milestones/:milestone_id" do + @milestone = user_project.milestones.find(params[:milestone_id]) + parameters = { + title: (params[:title] || @milestone.title), + description: (params[:description] || @milestone.description), + due_date: (params[:due_date] || @milestone.due_date), + closed: (params[:closed] || @milestone.closed) + } + + if @milestone.update_attributes(parameters) + present @milestone, with: Entities::Milestone + else + error!({'message' => '404 Not found'}, 404) + end + end + end + end +end diff --git a/lib/gitlab/markdown.rb b/lib/gitlab/markdown.rb index 75fa835d502..9a07133d0b3 100644 --- a/lib/gitlab/markdown.rb +++ b/lib/gitlab/markdown.rb @@ -100,7 +100,7 @@ module Gitlab def reference_commit(identifier) if commit = @project.commit(identifier) - link_to(identifier, project_commit_path(@project, id: commit.id), html_options.merge(title: "Commit: #{commit.author_name} - #{CommitDecorator.new(commit).title}", class: "gfm gfm-commit #{html_options[:class]}")) + link_to(identifier, project_commit_path(@project, id: commit.id), html_options.merge(title: CommitDecorator.new(commit).link_title, class: "gfm gfm-commit #{html_options[:class]}")) end end end diff --git a/lib/support/aws/debian_ubuntu_aws.sh b/lib/support/aws/debian_ubuntu_aws.sh new file mode 100644 index 00000000000..962ee008082 --- /dev/null +++ b/lib/support/aws/debian_ubuntu_aws.sh @@ -0,0 +1,125 @@ +#!/bin/sh + +# ABOUT +# This script performs a complete installation of Gitlab (master branch). +# Is can be run with one command without needing _any_ user input after that. +# This script only works on Amazon Web Services (AWS). +# The operating system used is Ubuntu 12.04 64bit. + +# TODO +# @dosire will send a pull request after this is merged in to change dosire/gitlabhq/non-interactive-aws-install links to gitlabhq/gitlabhq/master and reference this script from installation.md + +# HOWTO +# Signup for AWS, free tier are available at http://aws.amazon.com/free/ +# Go to EC2 tab in the AWS console EC2 https://console.aws.amazon.com/ec2/home +# Click the 'Launch Instance' button +# Select: 'Quick launch wizard' and continue +# Choose a key pair => Create New => Name it => Download it +# Choose a Launch Configuration => Select 'More Amazon Marketplace Images' +# Press 'Continue' +# Enter 'ubuntu/images/ubuntu-precise-12.04-amd64-server-20120424' and press 'Search' +# Select the only result (ami-3c994355) and press 'Continue' +# Press 'Edit details' if you want to modify something, for example make the type 'c1.medium' to make the install faster. +# Press the 'Launch' button +# Press 'Close' +# Click 'Security Groups' under the left hand menu 'NETWORK & SECURITY' +# Select the newly create seciruty group, probably named 'quicklaunch-1' +# Click on the Inbound tab +# In the 'Create a new rule' dropdown select 'HTTP' +# Press 'Add Rule' +# In the 'Create a new rule' dropdown select 'HTTPS' +# Press 'Add Rule' +# Press 'Apply Rule Changes' +# Give the following command in your local terminal while suptituting the UPPERCASE items +# 'ssh -i LOCATION_OF_AWS_KEY_PAIR_PRIVATE_KEY PUBLIC_DNS_OF_THE_NEW_SERVER' +# Execute the curl command below and when its ready follow the printed 'Log in instuctions' +# curl https://raw.github.com/dosire/gitlabhq/non-interactive-aws-install/lib/support/aws/debian_ubuntu_aws.sh | sh + +# Prevent fingerprint prompt for localhost in step 1 to 3. +echo "Host localhost + StrictHostKeyChecking no + UserKnownHostsFile=/dev/null" | sudo tee -a /etc/ssh/ssh_config + +# Existing script for Step 1 to 3 +curl https://raw.github.com/dosire/gitlabhq/non-interactive-aws-install/doc/debian_ubuntu.sh | sh + +# Install MySQL +sudo apt-get install -y makepasswd # Needed to create a unique password non-interactively. +userPassword=$(makepasswd --char=10) # Generate a random MySQL password +# Note that the lines below creates a cleartext copy of the random password in /var/cache/debconf/passwords.dat +# This file is normally only readable by root and the password will be deleted by the package management system after install. +echo mysql-server mysql-server/root_password password $userPassword | sudo debconf-set-selections +echo mysql-server mysql-server/root_password_again password $userPassword | sudo debconf-set-selections +sudo apt-get install -y mysql-server + +# Gitlab install +sudo gem install charlock_holmes --version '0.6.8' +sudo pip install pygments +sudo gem install bundler +sudo su -l gitlab -c "git clone git://github.com/gitlabhq/gitlabhq.git gitlab" # Using master everywhere. +sudo su -l gitlab -c "cd gitlab && mkdir tmp" +sudo su -l gitlab -c "cd gitlab/config && cp gitlab.yml.example gitlab.yml" +sudo su -l gitlab -c "cd gitlab/config && cp database.yml.example database.yml" +sudo sed -i 's/"secure password"/"'$userPassword'"/' /home/gitlab/gitlab/config/database.yml # Insert the mysql root password. +sudo su -l gitlab -c "cd gitlab && bundle install --without development test --deployment" +sudo su -l gitlab -c "cd gitlab && bundle exec rake gitlab:app:setup RAILS_ENV=production" + +# Setup gitlab hooks +sudo cp /home/gitlab/gitlab/lib/hooks/post-receive /home/git/share/gitolite/hooks/common/post-receive +sudo chown git:git /home/git/share/gitolite/hooks/common/post-receive + +# Set the first occurrence of host in the Gitlab config to the publicly available domain name +sudo sed -i '0,/host/s/localhost/'`wget -qO- http://instance-data/latest/meta-data/public-hostname`'/' /home/gitlab/gitlab/config/gitlab.yml + +# Gitlab installation test (optional) +# sudo -u gitlab bundle exec rake gitlab:app:status RAILS_ENV=production +# sudo -u gitlab bundle exec rails s -e production +# sudo -u gitlab bundle exec rake environment resque:work QUEUE=* RAILS_ENV=production BACKGROUND=no + +# Install and configure Nginx +sudo apt-get install -y nginx +sudo cp /home/gitlab/gitlab/lib/support/nginx-gitlab /etc/nginx/sites-available/gitlab +sudo ln -s /etc/nginx/sites-available/gitlab /etc/nginx/sites-enabled/gitlab +sudo sed -i 's/YOUR_SERVER_IP/'`wget -qO- http://instance-data/latest/meta-data/local-ipv4`'/' /etc/nginx/sites-available/gitlab # Set private ip address (public won't work). +sudo sed -i 's/YOUR_SERVER_FQDN/'`wget -qO- http://instance-data/latest/meta-data/public-hostname`'/' /etc/nginx/sites-available/gitlab # Set public dns domain name. + +# Configure Unicorn +sudo -u gitlab cp /home/gitlab/gitlab/config/unicorn.rb.orig /home/gitlab/gitlab/config/unicorn.rb + +# Create a Gitlab service +sudo cp /home/gitlab/gitlab/lib/support/init-gitlab /etc/init.d/gitlab +sudo chmod +x /etc/init.d/gitlab && sudo update-rc.d gitlab defaults + +## Gitlab service commands (unicorn and resque) +## restart doesn't restart resque, only start/stop effect it. +sudo -u gitlab service gitlab start +# sudo -u gitlab service gitlab restart +# sudo -u gitlab service gitlab stop + +# nginx Service commands +# sudo service nginx start +sudo service nginx restart +# sudo service nginx stop + +# Manual startup commands for troubleshooting when the service commands do not work +# sudo -u gitlab bundle exec unicorn_rails -c config/unicorn.rb -E production -D +# sudo su -l gitlab -c "cd gitlab && ./resque.sh" + +# Monitoring commands +# sudo tail -f /var/log/nginx/access.log; +# sudo tail -f /var/log/nginx/error.log; + +# Go to gitlab directory by default on next login. +echo 'cd /home/gitlab/gitlab' >> /home/ubuntu/.bashrc + +echo '' +echo '###########################################' +echo '# Log in instuctions #' +echo '###########################################' +echo '' +echo "Surf to this Gitlab installation in your browser:" +echo "http://`wget -qO- http://instance-data/latest/meta-data/public-hostname`/" +echo '' +echo 'and login with the following Email and Password:' +echo 'admin@local.host' +echo '5iveL!fe'
\ No newline at end of file diff --git a/resque_dev.sh b/resque_dev.sh index b09cfd9e383..0f1d6edb41f 100755 --- a/resque_dev.sh +++ b/resque_dev.sh @@ -1 +1,2 @@ -bundle exec rake environment resque:work QUEUE=post_receive,mailer,system_hook VVERBOSE=1 +mkdir -p tmp/pids +bundle exec rake environment resque:work QUEUE=post_receive,mailer,system_hook VVERBOSE=1 PIDFILE=tmp/pids/resque_worker.pid RAILS_ENV=development BACKGROUND=yes diff --git a/spec/factories.rb b/spec/factories.rb index 3f9673b499e..2e4acf39461 100644 --- a/spec/factories.rb +++ b/spec/factories.rb @@ -18,11 +18,15 @@ FactoryGirl.define do Faker::Lorem.sentence end + sequence :name, aliases: [:file_name] do + Faker::Name.name + end + sequence(:url) { Faker::Internet.uri('http') } factory :user, aliases: [:author, :assignee, :owner] do email { Faker::Internet.email } - name { Faker::Name.name } + name password "123456" password_confirmation "123456" @@ -116,6 +120,11 @@ FactoryGirl.define do author title content - file_name { Faker::Lorem.sentence } + file_name + end + + factory :protected_branch do + name + project end end diff --git a/spec/helpers/gitlab_flavored_markdown_spec.rb b/spec/helpers/gitlab_flavored_markdown_spec.rb deleted file mode 100644 index 28bd46ecb99..00000000000 --- a/spec/helpers/gitlab_flavored_markdown_spec.rb +++ /dev/null @@ -1,232 +0,0 @@ -require "spec_helper" - -describe GitlabMarkdownHelper do - before do - @project = Factory(:project) - @commit = @project.repo.commits.first.parents.first - @commit = CommitDecorator.decorate(Commit.new(@commit)) - @other_project = Factory :project, path: "OtherPath", code: "OtherCode" - @fake_user = Factory :user, name: "fred" - end - - describe "#gfm" do - it "should return text if @project is not set" do - @project = nil - - gfm("foo").should == "foo" - end - - describe "referencing a commit" do - it "should link using a full id" do - gfm("Reverts changes from #{@commit.id}").should == "Reverts changes from #{link_to @commit.id, project_commit_path(@project, id: @commit.id), title: "Commit: #{@commit.author_name} - #{@commit.title}", class: "gfm gfm-commit "}" - end - - it "should link using a short id" do - gfm("Backported from #{@commit.id[0, 6]}").should == "Backported from #{link_to @commit.id[0, 6], project_commit_path(@project, id: @commit.id), title: "Commit: #{@commit.author_name} - #{@commit.title}", class: "gfm gfm-commit "}" - end - - it "should link with adjecent text" do - gfm("Reverted (see #{@commit.id})").should == "Reverted (see #{link_to @commit.id, project_commit_path(@project, id: @commit.id), title: "Commit: #{@commit.author_name} - #{@commit.title}", class: "gfm gfm-commit "})" - end - - it "should not link with an invalid id" do - gfm("What happened in 12345678?").should == "What happened in 12345678?" - end - end - - describe "referencing a team member" do - it "should link using a simple name" do - user = Factory :user, name: "barry" - @project.users << user - member = @project.users_projects.where(user_id: user).first - - gfm("@#{user.name} you are right").should == "#{link_to "@#{user.name}", project_team_member_path(@project, member), class: "gfm gfm-team_member "} you are right" - end - - it "should link using a name with dots" do - user = Factory :user, name: "alphA.Beta" - @project.users << user - member = @project.users_projects.where(user_id: user).first - - gfm("@#{user.name} you are right").should == "#{link_to "@#{user.name}", project_team_member_path(@project, member), class: "gfm gfm-team_member "} you are right" - end - - it "should link using name with underscores" do - user = Factory :user, name: "ping_pong_king" - @project.users << user - member = @project.users_projects.where(user_id: user).first - - gfm("@#{user.name} you are right").should == "#{link_to "@#{user.name}", project_team_member_path(@project, member), class: "gfm gfm-team_member "} you are right" - end - - it "should link with adjecent text" do - user = Factory.create(:user, name: "ace") - @project.users << user - member = @project.users_projects.where(user_id: user).first - - gfm("Mail the Admin (@#{user.name})").should == "Mail the Admin (#{link_to "@#{user.name}", project_team_member_path(@project, member), class: "gfm gfm-team_member "})" - end - - it "should add styles" do - user = Factory :user, name: "barry" - @project.users << user - gfm("@#{user.name} you are right").should have_selector(".gfm.gfm-team_member") - end - - it "should not link using a bogus name" do - gfm("What hapened to @foo?").should == "What hapened to @foo?" - end - end - - describe "referencing an issue" do - before do - @issue = Factory :issue, assignee: @fake_user, author: @fake_user, project: @project - @invalid_issue = Factory :issue, assignee: @fake_user, author: @fake_user, project: @other_project - end - - it "should link using a correct id" do - gfm("Fixes ##{@issue.id}").should == "Fixes #{link_to "##{@issue.id}", project_issue_path(@project, @issue), title: "Issue: #{@issue.title}", class: "gfm gfm-issue "}" - end - - it "should link with adjecent text" do - gfm("This has already been discussed (see ##{@issue.id})").should == "This has already been discussed (see #{link_to "##{@issue.id}", project_issue_path(@project, @issue), title: "Issue: #{@issue.title}", class: "gfm gfm-issue "})" - end - - it "should add styles" do - gfm("Fixes ##{@issue.id}").should have_selector(".gfm.gfm-issue") - end - - it "should not link using an invalid id" do - gfm("##{@invalid_issue.id} has been marked duplicate of this").should == "##{@invalid_issue.id} has been marked duplicate of this" - end - end - - describe "referencing a merge request" do - before do - @merge_request = Factory :merge_request, assignee: @fake_user, author: @fake_user, project: @project - @invalid_merge_request = Factory :merge_request, assignee: @fake_user, author: @fake_user, project: @other_project - end - - it "should link using a correct id" do - gfm("Fixed in !#{@merge_request.id}").should == "Fixed in #{link_to "!#{@merge_request.id}", project_merge_request_path(@project, @merge_request), title: "Merge Request: #{@merge_request.title}", class: "gfm gfm-merge_request "}" - end - - it "should link with adjecent text" do - gfm("This has been fixed already (see !#{@merge_request.id})").should == "This has been fixed already (see #{link_to "!#{@merge_request.id}", project_merge_request_path(@project, @merge_request), title: "Merge Request: #{@merge_request.title}", class: "gfm gfm-merge_request "})" - end - - it "should add styles" do - gfm("Fixed in !#{@merge_request.id}").should have_selector(".gfm.gfm-merge_request") - end - - it "should not link using an invalid id" do - gfm("!#{@invalid_merge_request.id} violates our coding guidelines") - end - end - - describe "referencing a snippet" do - before do - @snippet = Factory.create(:snippet, - title: "Render asset to string", - author: @fake_user, - project: @project) - end - - it "should link using a correct id" do - gfm("Check out $#{@snippet.id}").should == "Check out #{link_to "$#{@snippet.id}", project_snippet_path(@project, @snippet), title: "Snippet: #{@snippet.title}", class: "gfm gfm-snippet "}" - end - - it "should link with adjecent text" do - gfm("I have created a snippet for that ($#{@snippet.id})").should == "I have created a snippet for that (#{link_to "$#{@snippet.id}", project_snippet_path(@project, @snippet), title: "Snippet: #{@snippet.title}", class: "gfm gfm-snippet "})" - end - - it "should add styles" do - gfm("Check out $#{@snippet.id}").should have_selector(".gfm.gfm-snippet") - end - - it "should not link using an invalid id" do - gfm("Don't use $1234").should == "Don't use $1234" - end - end - - it "should link to multiple things" do - user = Factory :user, name: "barry" - @project.users << user - member = @project.users_projects.where(user_id: user).first - - gfm("Let @#{user.name} fix the *mess* in #{@commit.id}").should == "Let #{link_to "@#{user.name}", project_team_member_path(@project, member), class: "gfm gfm-team_member "} fix the *mess* in #{link_to @commit.id, project_commit_path(@project, id: @commit.id), title: "Commit: #{@commit.author_name} - #{@commit.title}", class: "gfm gfm-commit "}" - end - - it "should not trip over other stuff" do - gfm("_Please_ *stop* 'helping' and all the other b*$#%' you do.").should == "_Please_ *stop* 'helping' and all the other b*$#%' you do." - end - - it "should not touch HTML entities" do - gfm("We'll accept good pull requests.").should == "We'll accept good pull requests." - end - - it "should forward HTML options to links" do - gfm("fixed in #{@commit.id}", class: "foo").should have_selector("a.foo") - end - end - - describe "#link_to_gfm" do - let(:issue1) { Factory :issue, assignee: @fake_user, author: @fake_user, project: @project } - let(:issue2) { Factory :issue, assignee: @fake_user, author: @fake_user, project: @project } - - it "should handle references nested in links with all the text" do - link_to_gfm("This should finally fix ##{issue1.id} and ##{issue2.id} for real", project_commit_path(@project, id: @commit.id)).should == "#{link_to "This should finally fix ", project_commit_path(@project, id: @commit.id)}#{link_to "##{issue1.id}", project_issue_path(@project, issue1), title: "Issue: #{issue1.title}", class: "gfm gfm-issue "}#{link_to " and ", project_commit_path(@project, id: @commit.id)}#{link_to "##{issue2.id}", project_issue_path(@project, issue2), title: "Issue: #{issue2.title}", class: "gfm gfm-issue "}#{link_to " for real", project_commit_path(@project, id: @commit.id)}" - end - - it "should forward HTML options" do - link_to_gfm("This should finally fix ##{issue1.id} for real", project_commit_path(@project, id: @commit.id), class: "foo").should have_selector(".foo") - end - end - - describe "#markdown" do - before do - @issue = Factory :issue, assignee: @fake_user, author: @fake_user, project: @project - @merge_request = Factory :merge_request, assignee: @fake_user, author: @fake_user, project: @project - @note = Factory.create(:note, - note: "Screenshot of the new feature", - project: @project, - noteable_id: @commit.id, - noteable_type: "Commit", - attachment: "screenshot123.jpg") - @snippet = Factory.create(:snippet, - title: "Render asset to string", - author: @fake_user, - project: @project) - - @other_user = Factory :user, name: "bill" - @project.users << @other_user - @member = @project.users_projects.where(user_id: @other_user).first - end - - it "should handle references in paragraphs" do - markdown("\n\nLorem ipsum dolor sit amet, consectetur adipiscing elit. #{@commit.id} Nam pulvinar sapien eget odio adipiscing at faucibus orci vestibulum.\n").should == "<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. #{link_to @commit.id, project_commit_path(@project, id: @commit.id), title: "Commit: #{@commit.author_name} - #{@commit.title}", class: "gfm gfm-commit "} Nam pulvinar sapien eget odio adipiscing at faucibus orci vestibulum.</p>\n" - end - - it "should handle references in headers" do - markdown("\n# Working around ##{@issue.id} for now\n## Apply !#{@merge_request.id}").should == "<h1 id=\"toc_0\">Working around #{link_to "##{@issue.id}", project_issue_path(@project, @issue), title: "Issue: #{@issue.title}", class: "gfm gfm-issue "} for now</h1>\n\n<h2 id=\"toc_1\">Apply #{link_to "!#{@merge_request.id}", project_merge_request_path(@project, @merge_request), title: "Merge Request: #{@merge_request.title}", class: "gfm gfm-merge_request "}</h2>\n" - end - - it "should handle references in lists" do - markdown("\n* dark: ##{@issue.id}\n* light by @#{@other_user.name}\n").should == "<ul>\n<li>dark: #{link_to "##{@issue.id}", project_issue_path(@project, @issue), title: "Issue: #{@issue.title}", class: "gfm gfm-issue "}</li>\n<li>light by #{link_to "@#{@other_user.name}", project_team_member_path(@project, @member), class: "gfm gfm-team_member "}</li>\n</ul>\n" - end - - it "should handle references in <em>" do - markdown("Apply _!#{@merge_request.id}_ ASAP").should == "<p>Apply <em>#{link_to "!#{@merge_request.id}", project_merge_request_path(@project, @merge_request), title: "Merge Request: #{@merge_request.title}", class: "gfm gfm-merge_request "}</em> ASAP</p>\n" - end - - it "should leave code blocks untouched" do - markdown("\n some code from $#{@snippet.id}\n here too\n").should == "<div class=\"highlight\"><pre><span class=\"n\">some</span> <span class=\"n\">code</span> <span class=\"n\">from</span> $#{@snippet.id}\n<span class=\"n\">here</span> <span class=\"n\">too</span>\n</pre>\n</div>\n" - - markdown("\n```\nsome code from $#{@snippet.id}\nhere too\n```\n").should == "<div class=\"highlight\"><pre><span class=\"n\">some</span> <span class=\"n\">code</span> <span class=\"n\">from</span> $#{@snippet.id}\n<span class=\"n\">here</span> <span class=\"n\">too</span>\n</pre>\n</div>\n" - end - - it "should leave inline code untouched" do - markdown("\nDon't use `$#{@snippet.id}` here.\n").should == "<p>Don't use <code>$#{@snippet.id}</code> here.</p>\n" - end - end -end diff --git a/spec/helpers/gitlab_markdown_helper_spec.rb b/spec/helpers/gitlab_markdown_helper_spec.rb new file mode 100644 index 00000000000..00164e0cdd7 --- /dev/null +++ b/spec/helpers/gitlab_markdown_helper_spec.rb @@ -0,0 +1,288 @@ +require "spec_helper" + +describe GitlabMarkdownHelper do + let!(:project) { create(:project) } + + let(:user) { create(:user, name: 'gfm') } + let(:commit) { CommitDecorator.decorate(project.commit) } + let(:issue) { create(:issue, project: project) } + let(:merge_request) { create(:merge_request, project: project) } + let(:snippet) { create(:snippet, project: project) } + let(:member) { project.users_projects.where(user_id: user).first } + + before do + # Helper expects a @project instance variable + @project = project + end + + describe "#gfm" do + it "should return unaltered text if project is nil" do + actual = "Testing references: ##{issue.id}" + + gfm(actual).should_not == actual + + @project = nil + gfm(actual).should == actual + end + + it "should not alter non-references" do + actual = expected = "_Please_ *stop* 'helping' and all the other b*$#%' you do." + gfm(actual).should == expected + end + + it "should not touch HTML entities" do + actual = expected = "We'll accept good pull requests." + gfm(actual).should == expected + end + + it "should forward HTML options to links" do + gfm("Fixed in #{commit.id}", class: "foo").should have_selector("a.gfm.foo") + end + + describe "referencing a commit" do + let(:expected) { project_commit_path(project, commit) } + + it "should link using a full id" do + actual = "Reverts #{commit.id}" + gfm(actual).should match(expected) + end + + it "should link using a short id" do + actual = "Backported from #{commit.short_id(6)}" + gfm(actual).should match(expected) + end + + it "should link with adjacent text" do + actual = "Reverted (see #{commit.id})" + gfm(actual).should match(expected) + end + + it "should keep whitespace intact" do + actual = "Changes #{commit.id} dramatically" + expected = /Changes <a.+>#{commit.id}<\/a> dramatically/ + gfm(actual).should match(expected) + end + + it "should not link with an invalid id" do + actual = expected = "What happened in #{commit.id.reverse}" + gfm(actual).should == expected + end + + it "should include a title attribute" do + actual = "Reverts #{commit.id}" + gfm(actual).should match(/title="#{commit.link_title}"/) + end + + it "should include standard gfm classes" do + actual = "Reverts #{commit.id}" + gfm(actual).should match(/class="\s?gfm gfm-commit\s?"/) + end + end + + describe "referencing a team member" do + let(:actual) { "@#{user.name} you are right." } + let(:expected) { project_team_member_path(project, member) } + + before do + project.users << user + end + + it "should link using a simple name" do + gfm(actual).should match(expected) + end + + it "should link using a name with dots" do + user.update_attributes(name: "alphA.Beta") + gfm(actual).should match(expected) + end + + it "should link using name with underscores" do + user.update_attributes(name: "ping_pong_king") + gfm(actual).should match(expected) + end + + it "should link with adjacent text" do + actual = "Mail the admin (@gfm)" + gfm(actual).should match(expected) + end + + it "should keep whitespace intact" do + actual = "Yes, @#{user.name} is right." + expected = /Yes, <a.+>@#{user.name}<\/a> is right/ + gfm(actual).should match(expected) + end + + it "should not link with an invalid id" do + actual = expected = "@#{user.name.reverse} you are right." + gfm(actual).should == expected + end + + it "should include standard gfm classes" do + gfm(actual).should match(/class="\s?gfm gfm-team_member\s?"/) + end + end + + # Shared examples for referencing an object + # + # Expects the following attributes to be available in the example group: + # + # - object - The object itself + # - reference - The object reference string (e.g., #1234, $1234, !1234) + # + # Currently limited to Snippets, Issues and MergeRequests + shared_examples 'referenced object' do + let(:actual) { "Reference to #{reference}" } + let(:expected) { polymorphic_path([project, object]) } + + it "should link using a valid id" do + gfm(actual).should match(expected) + end + + it "should link with adjacent text" do + # Wrap the reference in parenthesis + gfm(actual.gsub(reference, "(#{reference})")).should match(expected) + + # Append some text to the end of the reference + gfm(actual.gsub(reference, "#{reference}, right?")).should match(expected) + end + + it "should keep whitespace intact" do + actual = "Referenced #{reference} already." + expected = /Referenced <a.+>[^\s]+<\/a> already/ + gfm(actual).should match(expected) + end + + it "should not link with an invalid id" do + # Modify the reference string so it's still parsed, but is invalid + reference.gsub!(/^(.)(\d+)$/, '\1' + ('\2' * 2)) + gfm(actual).should == actual + end + + it "should include a title attribute" do + title = "#{object.class.to_s.titlecase}: #{object.title}" + gfm(actual).should match(/title="#{title}"/) + end + + it "should include standard gfm classes" do + css = object.class.to_s.underscore + gfm(actual).should match(/class="\s?gfm gfm-#{css}\s?"/) + end + end + + describe "referencing an issue" do + let(:object) { issue } + let(:reference) { "##{issue.id}" } + + include_examples 'referenced object' + end + + describe "referencing a merge request" do + let(:object) { merge_request } + let(:reference) { "!#{merge_request.id}" } + + include_examples 'referenced object' + end + + describe "referencing a snippet" do + let(:object) { snippet } + let(:reference) { "$#{snippet.id}" } + + include_examples 'referenced object' + end + + describe "referencing multiple objects" do + let(:actual) { "!#{merge_request.id} -> #{commit.id} -> ##{issue.id}" } + + it "should link to the merge request" do + expected = project_merge_request_path(project, merge_request) + gfm(actual).should match(expected) + end + + it "should link to the commit" do + expected = project_commit_path(project, commit) + gfm(actual).should match(expected) + end + + it "should link to the issue" do + expected = project_issue_path(project, issue) + gfm(actual).should match(expected) + end + end + end + + describe "#link_to_gfm" do + let(:commit_path) { project_commit_path(project, commit) } + let(:issues) { create_list(:issue, 2, project: project) } + + it "should handle references nested in links with all the text" do + actual = link_to_gfm("This should finally fix ##{issues[0].id} and ##{issues[1].id} for real", commit_path) + + # Break the result into groups of links with their content, without + # closing tags + groups = actual.split("</a>") + + # Leading commit link + groups[0].should match(/href="#{commit_path}"/) + groups[0].should match(/This should finally fix $/) + + # First issue link + groups[1].should match(/href="#{project_issue_path(project, issues[0])}"/) + groups[1].should match(/##{issues[0].id}$/) + + # Internal commit link + groups[2].should match(/href="#{commit_path}"/) + groups[2].should match(/ and /) + + # Second issue link + groups[3].should match(/href="#{project_issue_path(project, issues[1])}"/) + groups[3].should match(/##{issues[1].id}$/) + + # Trailing commit link + groups[4].should match(/href="#{commit_path}"/) + groups[4].should match(/ for real$/) + end + + it "should forward HTML options" do + actual = link_to_gfm("Fixed in #{commit.id}", commit_path, class: 'foo') + actual.should have_selector 'a.gfm.gfm-commit.foo' + end + end + + describe "#markdown" do + it "should handle references in paragraphs" do + markdown("\n\nLorem ipsum dolor sit amet, consectetur adipiscing elit. #{commit.id} Nam pulvinar sapien eget odio adipiscing at faucibus orci vestibulum.\n").should == "<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. #{link_to commit.id, project_commit_path(project, commit), title: commit.link_title, class: "gfm gfm-commit "} Nam pulvinar sapien eget odio adipiscing at faucibus orci vestibulum.</p>\n" + end + + it "should handle references in headers" do + actual = "\n# Working around ##{issue.id}\n## Apply !#{merge_request.id}" + + markdown(actual).should match(%r{<h1[^<]*>Working around <a.+>##{issue.id}</a></h1>}) + markdown(actual).should match(%r{<h2[^<]*>Apply <a.+>!#{merge_request.id}</a></h2>}) + end + + it "should handle references in lists" do + project.users << user + + actual = "\n* dark: ##{issue.id}\n* light by @#{member.user_name}" + + markdown(actual).should match(%r{<li>dark: <a.+>##{issue.id}</a></li>}) + markdown(actual).should match(%r{<li>light by <a.+>@#{member.user_name}</a></li>}) + end + + it "should handle references in <em>" do + actual = "Apply _!#{merge_request.id}_ ASAP" + + markdown(actual).should match(%r{Apply <em><a.+>!#{merge_request.id}</a></em>}) + end + + it "should leave code blocks untouched" do + markdown("\n some code from $#{snippet.id}\n here too\n").should == "<div class=\"highlight\"><pre><span class=\"n\">some</span> <span class=\"n\">code</span> <span class=\"n\">from</span> $#{snippet.id}\n<span class=\"n\">here</span> <span class=\"n\">too</span>\n</pre>\n</div>\n" + + markdown("\n```\nsome code from $#{snippet.id}\nhere too\n```\n").should == "<div class=\"highlight\"><pre><span class=\"n\">some</span> <span class=\"n\">code</span> <span class=\"n\">from</span> $#{snippet.id}\n<span class=\"n\">here</span> <span class=\"n\">too</span>\n</pre>\n</div>\n" + end + + it "should leave inline code untouched" do + markdown("\nDon't use `$#{snippet.id}` here.\n").should == "<p>Don't use <code>$#{snippet.id}</code> here.</p>\n" + end + end +end diff --git a/spec/mailers/notify_spec.rb b/spec/mailers/notify_spec.rb index 60f3231ce91..cf50b429f23 100644 --- a/spec/mailers/notify_spec.rb +++ b/spec/mailers/notify_spec.rb @@ -91,6 +91,29 @@ describe Notify do should have_body_text /#{project_issue_path project, issue}/ end end + + describe 'status changed' do + let(:current_user) { Factory.create :user, email: "current@email.com" } + let(:status) { 'closed' } + subject { Notify.issue_status_changed_email(recipient.id, issue.id, status, current_user) } + + it 'has the correct subject' do + should have_subject /changed issue ##{issue.id} \| #{issue.title}/i + end + + it 'contains the new status' do + should have_body_text /#{status}/i + end + + it 'contains the user name' do + should have_body_text /#{current_user.name}/i + end + + it 'contains a link to the issue' do + should have_body_text /#{project_issue_path project, issue}/ + end + end + end context 'for merge requests' do diff --git a/spec/models/event_spec.rb b/spec/models/event_spec.rb index 188f09978a7..aaffda3199e 100644 --- a/spec/models/event_spec.rb +++ b/spec/models/event_spec.rb @@ -1,24 +1,9 @@ -# == Schema Information -# -# Table name: events -# -# id :integer(4) not null, primary key -# target_type :string(255) -# target_id :integer(4) -# title :string(255) -# data :text -# project_id :integer(4) -# created_at :datetime not null -# updated_at :datetime not null -# action :integer(4) -# author_id :integer(4) -# - require 'spec_helper' describe Event do describe "Associations" do it { should belong_to(:project) } + it { should belong_to(:target) } end describe "Respond to" do @@ -29,16 +14,6 @@ describe Event do it { should respond_to(:commits) } end - describe "Creation" do - before do - @event = Factory :event - end - - it "should create a valid event" do - @event.should be_valid - end - end - describe "Push event" do before do project = Factory :project diff --git a/spec/models/issue_spec.rb b/spec/models/issue_spec.rb index 133f073451f..69829a4d13d 100644 --- a/spec/models/issue_spec.rb +++ b/spec/models/issue_spec.rb @@ -2,21 +2,16 @@ require 'spec_helper' describe Issue do describe "Associations" do - it { should belong_to(:project) } - it { should belong_to(:author) } - it { should belong_to(:assignee) } it { should belong_to(:milestone) } end describe "Validation" do - it { should validate_presence_of(:title) } - it { should validate_presence_of(:author_id) } - it { should validate_presence_of(:project_id) } + it { should ensure_length_of(:description).is_within(0..2000) } end - describe "Scope" do - it { Issue.should respond_to :closed } - it { Issue.should respond_to :opened } + describe 'modules' do + it { should include_module(IssueCommonality) } + it { should include_module(Upvote) } end subject { Factory.create(:issue) } @@ -61,57 +56,4 @@ describe Issue do subject.is_being_reopened?.should be_false end end - - describe "plus 1" do - subject { Factory.create(:issue) } - - it "with no notes has a 0/0 score" do - subject.upvotes.should == 0 - end - - it "should recognize non-+1 notes" do - subject.notes << Factory(:note, note: "No +1 here") - subject.should have(1).note - subject.notes.first.upvote?.should be_false - subject.upvotes.should == 0 - end - - it "should recognize a single +1 note" do - subject.notes << Factory(:note, note: "+1 This is awesome") - subject.upvotes.should == 1 - end - - it "should recognize a multiple +1 notes" do - subject.notes << Factory(:note, note: "+1 This is awesome") - subject.notes << Factory(:note, note: "+1 I want this") - subject.upvotes.should == 2 - end - end - - describe ".search" do - let!(:issue) { Factory.create(:issue, title: "Searchable issue") } - - it "matches by title" do - Issue.search('able').all.should == [issue] - end - end end -# == Schema Information -# -# Table name: issues -# -# id :integer(4) not null, primary key -# title :string(255) -# assignee_id :integer(4) -# author_id :integer(4) -# project_id :integer(4) -# created_at :datetime not null -# updated_at :datetime not null -# closed :boolean(1) default(FALSE), not null -# position :integer(4) default(0) -# critical :boolean(1) default(FALSE), not null -# branch_name :string(255) -# description :text -# milestone_id :integer(4) -# - diff --git a/spec/models/key_spec.rb b/spec/models/key_spec.rb index ea58fbd291e..85cd291d681 100644 --- a/spec/models/key_spec.rb +++ b/spec/models/key_spec.rb @@ -2,12 +2,15 @@ require 'spec_helper' describe Key do describe "Associations" do - it { should belong_to(:user) or belong_to(:project) } + it { should belong_to(:user) } + it { should belong_to(:project) } end describe "Validation" do it { should validate_presence_of(:title) } it { should validate_presence_of(:key) } + it { should ensure_length_of(:title).is_within(0..255) } + it { should ensure_length_of(:key).is_within(0..5000) } end describe "Methods" do @@ -44,17 +47,3 @@ describe Key do end end end -# == Schema Information -# -# Table name: keys -# -# id :integer(4) not null, primary key -# user_id :integer(4) -# created_at :datetime not null -# updated_at :datetime not null -# key :text -# title :string(255) -# identifier :string(255) -# project_id :integer(4) -# - diff --git a/spec/models/merge_request_spec.rb b/spec/models/merge_request_spec.rb index f4b93eea966..d1253b35ae5 100644 --- a/spec/models/merge_request_spec.rb +++ b/spec/models/merge_request_spec.rb @@ -1,76 +1,13 @@ require 'spec_helper' describe MergeRequest do - describe "Associations" do - it { should belong_to(:project) } - it { should belong_to(:author) } - it { should belong_to(:assignee) } - end - describe "Validation" do it { should validate_presence_of(:target_branch) } it { should validate_presence_of(:source_branch) } - it { should validate_presence_of(:title) } - it { should validate_presence_of(:author_id) } - it { should validate_presence_of(:project_id) } - end - - describe "Scope" do - it { MergeRequest.should respond_to :closed } - it { MergeRequest.should respond_to :opened } - end - - describe "plus 1" do - subject { Factory.create(:merge_request) } - - it "with no notes has a 0/0 score" do - subject.upvotes.should == 0 - end - - it "should recognize non-+1 notes" do - subject.notes << Factory(:note, note: "No +1 here") - subject.should have(1).note - subject.notes.first.upvote?.should be_false - subject.upvotes.should == 0 - end - - it "should recognize a single +1 note" do - subject.notes << Factory(:note, note: "+1 This is awesome") - subject.upvotes.should == 1 - end - - it "should recognize a multiple +1 notes" do - subject.notes << Factory(:note, note: "+1 This is awesome") - subject.notes << Factory(:note, note: "+1 I want this") - subject.upvotes.should == 2 - end end - describe ".search" do - let!(:issue) { Factory.create(:issue, title: "Searchable issue") } - - it "matches by title" do - Issue.search('able').all.should == [issue] - end + describe 'modules' do + it { should include_module(IssueCommonality) } + it { should include_module(Upvote) } end end -# == Schema Information -# -# Table name: merge_requests -# -# id :integer(4) not null, primary key -# target_branch :string(255) not null -# source_branch :string(255) not null -# project_id :integer(4) not null -# author_id :integer(4) -# assignee_id :integer(4) -# title :string(255) -# closed :boolean(1) default(FALSE), not null -# created_at :datetime not null -# updated_at :datetime not null -# st_commits :text(2147483647 -# st_diffs :text(2147483647 -# merged :boolean(1) default(FALSE), not null -# state :integer(4) default(1), not null -# - diff --git a/spec/models/milestone_spec.rb b/spec/models/milestone_spec.rb index ed805a243b5..fa15fc8f560 100644 --- a/spec/models/milestone_spec.rb +++ b/spec/models/milestone_spec.rb @@ -1,17 +1,3 @@ -# == Schema Information -# -# Table name: milestones -# -# id :integer(4) not null, primary key -# title :string(255) not null -# project_id :integer(4) not null -# description :text -# due_date :date -# closed :boolean(1) default(FALSE), not null -# created_at :datetime not null -# updated_at :datetime not null -# - require 'spec_helper' describe Milestone do diff --git a/spec/models/note_spec.rb b/spec/models/note_spec.rb index 89e50479762..ffaf442d9a4 100644 --- a/spec/models/note_spec.rb +++ b/spec/models/note_spec.rb @@ -3,6 +3,8 @@ require 'spec_helper' describe Note do describe "Associations" do it { should belong_to(:project) } + it { should belong_to(:noteable) } + it { should belong_to(:author).class_name('User') } end describe "Validation" do @@ -130,19 +132,3 @@ describe Note do end end end -# == Schema Information -# -# Table name: notes -# -# id :integer(4) not null, primary key -# note :text -# noteable_id :string(255) -# noteable_type :string(255) -# author_id :integer(4) -# created_at :datetime not null -# updated_at :datetime not null -# project_id :integer(4) -# attachment :string(255) -# line_code :string(255) -# - diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb index faaa9a917f4..5add7ff88a9 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -2,23 +2,52 @@ require 'spec_helper' describe Project do describe "Associations" do + it { should belong_to(:owner).class_name('User') } it { should have_many(:users) } - it { should have_many(:protected_branches).dependent(:destroy) } it { should have_many(:events).dependent(:destroy) } - it { should have_many(:wikis).dependent(:destroy) } it { should have_many(:merge_requests).dependent(:destroy) } - it { should have_many(:users_projects).dependent(:destroy) } it { should have_many(:issues).dependent(:destroy) } + it { should have_many(:milestones).dependent(:destroy) } + it { should have_many(:users_projects).dependent(:destroy) } it { should have_many(:notes).dependent(:destroy) } it { should have_many(:snippets).dependent(:destroy) } - it { should have_many(:hooks).dependent(:destroy) } it { should have_many(:deploy_keys).dependent(:destroy) } + it { should have_many(:hooks).dependent(:destroy) } + it { should have_many(:wikis).dependent(:destroy) } + it { should have_many(:protected_branches).dependent(:destroy) } end describe "Validation" do + let!(:project) { create(:project) } + it { should validate_presence_of(:name) } + it { should validate_uniqueness_of(:name) } + it { should ensure_length_of(:name).is_within(0..255) } + it { should validate_presence_of(:path) } + it { should validate_uniqueness_of(:path) } + it { should ensure_length_of(:path).is_within(0..255) } + # TODO: Formats + + it { should ensure_length_of(:description).is_within(0..2000) } + it { should validate_presence_of(:code) } + it { should validate_uniqueness_of(:code) } + it { should ensure_length_of(:code).is_within(1..255) } + # TODO: Formats + + it { should validate_presence_of(:owner) } + + it "should not allow new projects beyond user limits" do + project.stub(:owner).and_return(double(can_create_project?: false, projects_limit: 1)) + project.should_not be_valid + project.errors[:base].first.should match(/Your own projects limit is 1/) + end + + it "should not allow 'gitolite-admin' as repo name" do + should allow_value("blah").for(:path) + should_not allow_value("gitolite-admin").for(:path) + end end describe "Respond to" do @@ -73,9 +102,11 @@ describe Project do it { should respond_to(:trigger_post_receive) } end - it "should not allow 'gitolite-admin' as repo name" do - should allow_value("blah").for(:path) - should_not allow_value("gitolite-admin").for(:path) + describe 'modules' do + it { should include_module(Repository) } + it { should include_module(PushObserver) } + it { should include_module(Authority) } + it { should include_module(Team) } end it "should return valid url to repo" do @@ -110,7 +141,7 @@ describe Project do let(:last_event) { double } before do - project.stub(:events).and_return( [ double, double, last_event ] ) + project.stub_chain(:events, :order).and_return( [ double, double, last_event ] ) end it { project.last_activity.should == last_event } @@ -236,23 +267,3 @@ describe Project do end end end -# == Schema Information -# -# Table name: projects -# -# id :integer(4) not null, primary key -# name :string(255) -# path :string(255) -# description :text -# created_at :datetime not null -# updated_at :datetime not null -# private_flag :boolean(1) default(TRUE), not null -# code :string(255) -# owner_id :integer(4) -# default_branch :string(255) default("master"), not null -# issues_enabled :boolean(1) default(TRUE), not null -# wall_enabled :boolean(1) default(TRUE), not null -# merge_requests_enabled :boolean(1) default(TRUE), not null -# wiki_enabled :boolean(1) default(TRUE), not null -# - diff --git a/spec/models/protected_branch_spec.rb b/spec/models/protected_branch_spec.rb index 1654e3b6f56..9180bc3bca6 100644 --- a/spec/models/protected_branch_spec.rb +++ b/spec/models/protected_branch_spec.rb @@ -1,19 +1,6 @@ -# == Schema Information -# -# Table name: protected_branches -# -# id :integer(4) not null, primary key -# project_id :integer(4) not null -# name :string(255) not null -# created_at :datetime not null -# updated_at :datetime not null -# - require 'spec_helper' describe ProtectedBranch do - let(:project) { Factory(:project) } - describe 'Associations' do it { should belong_to(:project) } end @@ -24,26 +11,26 @@ describe ProtectedBranch do end describe 'Callbacks' do - subject { ProtectedBranch.new(project: project, name: 'branch_name') } + let(:branch) { build(:protected_branch) } it 'call update_repository after save' do - subject.should_receive(:update_repository) - subject.save + branch.should_receive(:update_repository) + branch.save end it 'call update_repository after destroy' do - subject.should_receive(:update_repository) - subject.destroy + branch.save + branch.should_receive(:update_repository) + branch.destroy end end describe '#commit' do - subject { ProtectedBranch.new(project: project, name: 'cant_touch_this') } + let(:branch) { create(:protected_branch) } it 'commits itself to its project' do - project.should_receive(:commit).with('cant_touch_this') - - subject.commit + branch.project.should_receive(:commit).with(branch.name) + branch.commit end end end diff --git a/spec/models/snippet_spec.rb b/spec/models/snippet_spec.rb index 9b4aaa13f74..ffb861c4910 100644 --- a/spec/models/snippet_spec.rb +++ b/spec/models/snippet_spec.rb @@ -3,29 +3,21 @@ require 'spec_helper' describe Snippet do describe "Associations" do it { should belong_to(:project) } - it { should belong_to(:author) } + it { should belong_to(:author).class_name('User') } + it { should have_many(:notes).dependent(:destroy) } end describe "Validation" 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(:title) } + it { should ensure_length_of(:title).is_within(0..255) } + it { should validate_presence_of(:file_name) } + it { should ensure_length_of(:title).is_within(0..255) } + it { should validate_presence_of(:content) } + it { should ensure_length_of(:content).is_within(0..10_000) } end end -# == Schema Information -# -# Table name: snippets -# -# id :integer(4) not null, primary key -# title :string(255) -# content :text -# author_id :integer(4) not null -# project_id :integer(4) not null -# created_at :datetime not null -# updated_at :datetime not null -# file_name :string(255) -# expires_at :datetime -# - diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index ebc45fa4710..ca34f07df7f 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -2,13 +2,26 @@ require 'spec_helper' describe User do describe "Associations" do - it { should have_many(:projects) } it { should have_many(:users_projects).dependent(:destroy) } + it { should have_many(:projects) } + it { should have_many(:my_own_projects).class_name('Project') } + it { should have_many(:keys).dependent(:destroy) } + it { should have_many(:events).class_name('Event').dependent(:destroy) } + it { should have_many(:recent_events).class_name('Event') } it { should have_many(:issues).dependent(:destroy) } + it { should have_many(:notes).dependent(:destroy) } it { should have_many(:assigned_issues).dependent(:destroy) } it { should have_many(:merge_requests).dependent(:destroy) } it { should have_many(:assigned_merge_requests).dependent(:destroy) } - it { should have_many(:notes).dependent(:destroy) } + end + + describe 'validations' do + it { should validate_presence_of(:projects_limit) } + it { should validate_numericality_of(:projects_limit) } + it { should allow_value(0).for(:projects_limit) } + it { should_not allow_value(-1).for(:projects_limit) } + + it { should ensure_length_of(:bio).is_within(0..255) } end describe "Respond to" do @@ -51,33 +64,3 @@ describe User do user.authentication_token.should_not == "" end end -# == Schema Information -# -# Table name: users -# -# id :integer(4) not null, primary key -# email :string(255) default(""), not null -# encrypted_password :string(128) default(""), not null -# reset_password_token :string(255) -# reset_password_sent_at :datetime -# remember_created_at :datetime -# sign_in_count :integer(4) default(0) -# current_sign_in_at :datetime -# last_sign_in_at :datetime -# current_sign_in_ip :string(255) -# last_sign_in_ip :string(255) -# created_at :datetime not null -# updated_at :datetime not null -# name :string(255) -# admin :boolean(1) default(FALSE), not null -# projects_limit :integer(4) default(10) -# skype :string(255) default(""), not null -# linkedin :string(255) default(""), not null -# twitter :string(255) default(""), not null -# authentication_token :string(255) -# dark_scheme :boolean(1) default(FALSE), not null -# theme_id :integer(4) default(1), not null -# bio :string(255) -# blocked :boolean(1) default(FALSE), not null -# - diff --git a/spec/models/users_project_spec.rb b/spec/models/users_project_spec.rb index 87fbfbf2a8c..3197ba6eb6b 100644 --- a/spec/models/users_project_spec.rb +++ b/spec/models/users_project_spec.rb @@ -7,7 +7,11 @@ describe UsersProject do end describe "Validation" do + let!(:users_project) { create(:users_project) } + it { should validate_presence_of(:user_id) } + it { should validate_uniqueness_of(:user_id).scoped_to(:project_id) } + it { should validate_presence_of(:project_id) } end @@ -16,15 +20,3 @@ describe UsersProject do it { should respond_to(:user_email) } end end -# == Schema Information -# -# Table name: users_projects -# -# id :integer(4) not null, primary key -# user_id :integer(4) not null -# project_id :integer(4) not null -# created_at :datetime not null -# updated_at :datetime not null -# project_access :integer(4) default(0), not null -# - diff --git a/spec/models/web_hook_spec.rb b/spec/models/web_hook_spec.rb index 885947614d7..3cba5b64ff0 100644 --- a/spec/models/web_hook_spec.rb +++ b/spec/models/web_hook_spec.rb @@ -52,14 +52,3 @@ describe ProjectHook do end end end -# == Schema Information -# -# Table name: web_hooks -# -# id :integer(4) not null, primary key -# url :string(255) -# project_id :integer(4) -# created_at :datetime not null -# updated_at :datetime not null -# - diff --git a/spec/models/wiki_spec.rb b/spec/models/wiki_spec.rb index 892d0e8fe17..de6ce426331 100644 --- a/spec/models/wiki_spec.rb +++ b/spec/models/wiki_spec.rb @@ -4,27 +4,13 @@ describe Wiki do describe "Associations" do it { should belong_to(:project) } it { should belong_to(:user) } + it { should have_many(:notes).dependent(:destroy) } end describe "Validation" do it { should validate_presence_of(:title) } + it { should ensure_length_of(:title).is_within(1..250) } it { should validate_presence_of(:content) } it { should validate_presence_of(:user_id) } end - - it { Factory(:wiki).should be_valid } end -# == Schema Information -# -# Table name: wikis -# -# id :integer(4) not null, primary key -# title :string(255) -# content :text -# project_id :integer(4) -# created_at :datetime not null -# updated_at :datetime not null -# slug :string(255) -# user_id :integer(4) -# - diff --git a/spec/observers/issue_observer_spec.rb b/spec/observers/issue_observer_spec.rb index c6a405f1c1b..b5943f2c539 100644 --- a/spec/observers/issue_observer_spec.rb +++ b/spec/observers/issue_observer_spec.rb @@ -3,7 +3,8 @@ require 'spec_helper' describe IssueObserver do let(:some_user) { double(:user, id: 1) } let(:assignee) { double(:user, id: 2) } - let(:issue) { double(:issue, id: 42, assignee: assignee) } + let(:author) { double(:user, id: 3) } + let(:issue) { double(:issue, id: 42, assignee: assignee, author: author) } before(:each) { subject.stub(:current_user).and_return(some_user) } @@ -67,36 +68,90 @@ describe IssueObserver do end end - context 'a status "closed" note' do - it 'is created if the issue is being closed' do + context 'a status "closed"' do + it 'note is created if the issue is being closed' do issue.should_receive(:is_being_closed?).and_return(true) Note.should_receive(:create_status_change_note).with(issue, some_user, 'closed') subject.after_update(issue) end - it 'is not created if the issue is not being closed' do + it 'note is not created if the issue is not being closed' do issue.should_receive(:is_being_closed?).and_return(false) Note.should_not_receive(:create_status_change_note).with(issue, some_user, 'closed') subject.after_update(issue) end + + it 'notification is delivered if the issue being closed' do + issue.stub(:is_being_closed?).and_return(true) + Notify.should_receive(:issue_status_changed_email).twice + Note.should_receive(:create_status_change_note).with(issue, some_user, 'closed') + + subject.after_update(issue) + end + + it 'notification is not delivered if the issue not being closed' do + issue.stub(:is_being_closed?).and_return(false) + Notify.should_not_receive(:issue_status_changed_email) + Note.should_not_receive(:create_status_change_note).with(issue, some_user, 'closed') + + subject.after_update(issue) + end + + it 'notification is delivered only to author if the issue being closed' do + issue_without_assignee = double(:issue, id: 42, author: author, assignee: nil) + issue_without_assignee.stub(:is_being_reassigned?).and_return(false) + issue_without_assignee.stub(:is_being_closed?).and_return(true) + issue_without_assignee.stub(:is_being_reopened?).and_return(false) + Notify.should_receive(:issue_status_changed_email).once + Note.should_receive(:create_status_change_note).with(issue_without_assignee, some_user, 'closed') + + subject.after_update(issue_without_assignee) + end end - context 'a status "reopened" note' do - it 'is created if the issue is being reopened' do + context 'a status "reopened"' do + it 'note is created if the issue is being reopened' do issue.should_receive(:is_being_reopened?).and_return(true) Note.should_receive(:create_status_change_note).with(issue, some_user, 'reopened') subject.after_update(issue) end - it 'is not created if the issue is not being reopened' do + it 'note is not created if the issue is not being reopened' do issue.should_receive(:is_being_reopened?).and_return(false) Note.should_not_receive(:create_status_change_note).with(issue, some_user, 'reopened') subject.after_update(issue) end + + it 'notification is delivered if the issue being reopened' do + issue.stub(:is_being_reopened?).and_return(true) + Notify.should_receive(:issue_status_changed_email).twice + Note.should_receive(:create_status_change_note).with(issue, some_user, 'reopened') + + subject.after_update(issue) + end + + it 'notification is not delivered if the issue not being reopened' do + issue.stub(:is_being_reopened?).and_return(false) + Notify.should_not_receive(:issue_status_changed_email) + Note.should_not_receive(:create_status_change_note).with(issue, some_user, 'reopened') + + subject.after_update(issue) + end + + it 'notification is delivered only to author if the issue being reopened' do + issue_without_assignee = double(:issue, id: 42, author: author, assignee: nil) + issue_without_assignee.stub(:is_being_reassigned?).and_return(false) + issue_without_assignee.stub(:is_being_closed?).and_return(false) + issue_without_assignee.stub(:is_being_reopened?).and_return(true) + Notify.should_receive(:issue_status_changed_email).once + Note.should_receive(:create_status_change_note).with(issue_without_assignee, some_user, 'reopened') + + subject.after_update(issue_without_assignee) + end end end diff --git a/spec/requests/api/issues_spec.rb b/spec/requests/api/issues_spec.rb index c00a056d079..293ea83ae6c 100644 --- a/spec/requests/api/issues_spec.rb +++ b/spec/requests/api/issues_spec.rb @@ -65,9 +65,8 @@ describe Gitlab::API do describe "DELETE /projects/:id/issues/:issue_id" do it "should delete a project issue" do - expect { - delete api("/projects/#{project.code}/issues/#{issue.id}", user) - }.to change { Issue.count }.by(-1) + delete api("/projects/#{project.code}/issues/#{issue.id}", user) + response.status.should == 405 end end end diff --git a/spec/requests/api/milestones_spec.rb b/spec/requests/api/milestones_spec.rb new file mode 100644 index 00000000000..cf5f65f068c --- /dev/null +++ b/spec/requests/api/milestones_spec.rb @@ -0,0 +1,47 @@ +require 'spec_helper' + +describe Gitlab::API do + include ApiHelpers + + let(:user) { Factory :user } + let!(:project) { Factory :project, owner: user } + let!(:milestone) { Factory :milestone, project: project } + + before { project.add_access(user, :read) } + + describe "GET /projects/:id/milestones" do + it "should return project milestones" do + get api("/projects/#{project.code}/milestones", user) + response.status.should == 200 + json_response.should be_an Array + json_response.first['title'].should == milestone.title + end + end + + describe "GET /projects/:id/milestones/:milestone_id" do + it "should return a project milestone by id" do + get api("/projects/#{project.code}/milestones/#{milestone.id}", user) + response.status.should == 200 + json_response['title'].should == milestone.title + end + end + + describe "POST /projects/:id/milestones" do + it "should create a new project milestone" do + post api("/projects/#{project.code}/milestones", user), + title: 'new milestone' + response.status.should == 201 + json_response['title'].should == 'new milestone' + json_response['description'].should be_nil + end + end + + describe "PUT /projects/:id/milestones/:milestone_id" do + it "should update a project milestone" do + put api("/projects/#{project.code}/milestones/#{milestone.id}", user), + title: 'updated title' + response.status.should == 200 + json_response['title'].should == 'updated title' + end + end +end diff --git a/spec/roles/issue_commonality_spec.rb b/spec/roles/issue_commonality_spec.rb new file mode 100644 index 00000000000..77b98b46ed9 --- /dev/null +++ b/spec/roles/issue_commonality_spec.rb @@ -0,0 +1,69 @@ +require 'spec_helper' + +describe Issue, "IssueCommonality" do + let(:issue) { create(:issue) } + + describe "Associations" do + it { should belong_to(:project) } + it { should belong_to(:author) } + it { should belong_to(:assignee) } + it { should have_many(:notes).dependent(:destroy) } + end + + describe "Validation" do + it { should validate_presence_of(:project_id) } + it { should validate_presence_of(:author_id) } + it { should validate_presence_of(:title) } + it { should ensure_length_of(:title).is_at_least(0).is_at_most(255) } + end + + describe "Scope" do + it { described_class.should respond_to(:opened) } + it { described_class.should respond_to(:closed) } + it { described_class.should respond_to(:assigned) } + end + + it "has an :author_id_of_changes accessor" do + issue.should respond_to(:author_id_of_changes) + issue.should respond_to(:author_id_of_changes=) + end + + describe ".search" do + let!(:searchable_issue) { create(:issue, title: "Searchable issue") } + + it "matches by title" do + described_class.search('able').all.should == [searchable_issue] + end + end + + describe "#today?" do + it "returns true when created today" do + # Avoid timezone differences and just return exactly what we want + Date.stub(:today).and_return(issue.created_at.to_date) + issue.today?.should be_true + end + + it "returns false when not created today" do + Date.stub(:today).and_return(Date.yesterday) + issue.today?.should be_false + end + end + + describe "#new?" do + it "returns true when created today and record hasn't been updated" do + issue.stub(:today?).and_return(true) + issue.new?.should be_true + end + + it "returns false when not created today" do + issue.stub(:today?).and_return(false) + issue.new?.should be_false + end + + it "returns false when record has been updated" do + issue.stub(:today?).and_return(true) + issue.touch + issue.new?.should be_false + end + end +end diff --git a/spec/roles/upvote_spec.rb b/spec/roles/upvote_spec.rb new file mode 100644 index 00000000000..24288ada0fe --- /dev/null +++ b/spec/roles/upvote_spec.rb @@ -0,0 +1,27 @@ +require 'spec_helper' + +describe Issue, "Upvote" do + let(:issue) { create(:issue) } + + it "with no notes has a 0/0 score" do + issue.upvotes.should == 0 + end + + it "should recognize non-+1 notes" do + issue.notes << create(:note, note: "No +1 here") + issue.should have(1).note + issue.notes.first.upvote?.should be_false + issue.upvotes.should == 0 + end + + it "should recognize a single +1 note" do + issue.notes << create(:note, note: "+1 This is awesome") + issue.upvotes.should == 1 + end + + it "should recognize multiple +1 notes" do + issue.notes << create(:note, note: "+1 This is awesome") + issue.notes << create(:note, note: "+1 I want this") + issue.upvotes.should == 2 + end +end diff --git a/spec/support/matchers.rb b/spec/support/matchers.rb index e0672166e92..cb1dcba3dd8 100644 --- a/spec/support/matchers.rb +++ b/spec/support/matchers.rb @@ -28,6 +28,16 @@ RSpec::Matchers.define :be_404_for do |user| end end +RSpec::Matchers.define :include_module do |expected| + match do + described_class.included_modules.include?(expected) + end + + failure_message_for_should do + "expected #{described_class} to include the #{expected} module" + end +end + module UrlAccess def url_allowed?(user, url) emulate_user(user) @@ -57,3 +67,17 @@ module UrlAccess login_with(user) if user end end + +# Extend shoulda-matchers +module Shoulda::Matchers::ActiveModel + class EnsureLengthOfMatcher + # Shortcut for is_at_least and is_at_most + def is_within(range) + if range.exclude_end? + is_at_least(range.first) && is_at_most(range.last - 1) + else + is_at_least(range.first) && is_at_most(range.last) + end + end + end +end |