Commit eca823c1c7cef45cc18c6ab36d2327650c85bfc3

Authored by Nihad Abbasov
2 parents 024e0348 8b7e404b

Merge branch 'master' into api

Showing 125 changed files with 1752 additions and 666 deletions   Show diff stats
.gitignore
... ... @@ -6,6 +6,7 @@ log/*.log
6 6 tmp/
7 7 .sass-cache/
8 8 coverage/*
  9 +backups/*
9 10 *.swp
10 11 public/uploads/
11 12 .rvmrc
... ...
CHANGELOG
1 1 v 2.7.0
2 2 - Issue Labels
  3 + - Inline diff
  4 + - Git HTTP
  5 + - API
  6 + - UI improved
  7 + - System hooks
  8 + - UI improved
  9 + - Dashboard events endless scroll
  10 + - Source perfomance increased
3 11  
4 12 v 2.6.0
5 13 - UI polished
... ...
Gemfile
... ... @@ -7,7 +7,7 @@ gem "sqlite3"
7 7 gem "mysql2"
8 8  
9 9 # Auth
10   -gem "devise", "~> 1.5"
  10 +gem "devise", "~> 2.1.0"
11 11  
12 12 # GITLAB patched libs
13 13 gem "grit", :git => "https://github.com/gitlabhq/grit.git", :ref => "7f35cb98ff17d534a07e3ce6ec3d580f67402837"
... ... @@ -71,7 +71,6 @@ group :development, :test do
71 71 gem "awesome_print"
72 72 gem "database_cleaner"
73 73 gem "launchy"
74   - gem "webmock"
75 74 end
76 75  
77 76 group :test do
... ... @@ -82,4 +81,5 @@ group :test do
82 81 gem "shoulda-matchers"
83 82 gem 'email_spec'
84 83 gem 'resque_spec'
  84 + gem "webmock"
85 85 end
... ...
Gemfile.lock
... ... @@ -148,10 +148,11 @@ GEM
148 148 nokogiri (>= 1.5.0)
149 149 daemons (1.1.8)
150 150 database_cleaner (0.8.0)
151   - devise (1.5.3)
  151 + devise (2.1.2)
152 152 bcrypt-ruby (~> 3.0)
153   - orm_adapter (~> 0.0.3)
154   - warden (~> 1.1)
  153 + orm_adapter (~> 0.1)
  154 + railties (~> 3.1)
  155 + warden (~> 1.2.1)
155 156 diff-lcs (1.1.3)
156 157 drapper (0.8.4)
157 158 email_spec (1.2.1)
... ... @@ -225,7 +226,7 @@ GEM
225 226 omniauth (1.1.0)
226 227 hashie (~> 1.2)
227 228 rack
228   - orm_adapter (0.0.7)
  229 + orm_adapter (0.3.0)
229 230 polyglot (0.3.3)
230 231 posix-spawn (0.3.6)
231 232 pry (0.9.9.6)
... ... @@ -356,7 +357,7 @@ GEM
356 357 raindrops (~> 0.7)
357 358 vegas (0.1.11)
358 359 rack (>= 1.0.0)
359   - warden (1.2.0)
  360 + warden (1.2.1)
360 361 rack (>= 1.0)
361 362 webmock (1.8.7)
362 363 addressable (>= 2.2.7)
... ... @@ -383,7 +384,7 @@ DEPENDENCIES
383 384 colored
384 385 cucumber-rails
385 386 database_cleaner
386   - devise (~> 1.5)
  387 + devise (~> 2.1.0)
387 388 drapper
388 389 email_spec
389 390 ffaker
... ...
VERSION
1   -2.7.0pre
  1 +2.7.0
... ...
app/assets/images/ajax_loader_tree.gif 0 → 100644

6.38 KB

app/assets/javascripts/application.js
... ... @@ -12,6 +12,7 @@
12 12 //= require jquery.cookie
13 13 //= require jquery.endless-scroll
14 14 //= require jquery.highlight
  15 +//= require jquery.waitforimages
15 16 //= require bootstrap-modal
16 17 //= require modernizr
17 18 //= require chosen-jquery
... ... @@ -20,10 +21,26 @@
20 21 //= require_tree .
21 22  
22 23 $(document).ready(function(){
  24 +
23 25 $(".one_click_select").live("click", function(){
24 26 $(this).select();
25 27 });
26 28  
  29 +
  30 + $('body').on('ajax:complete, ajax:beforeSend, submit', 'form', function(e){
  31 + var buttons = $('[type="submit"]', this);
  32 + switch( e.type ){
  33 + case 'ajax:beforeSend':
  34 + case 'submit':
  35 + buttons.attr('disabled', 'disabled');
  36 + break;
  37 + case ' ajax:complete':
  38 + default:
  39 + buttons.removeAttr('disabled');
  40 + break;
  41 + }
  42 + })
  43 +
27 44 $(".account-box").mouseenter(showMenu);
28 45 $(".account-box").mouseleave(resetMenu);
29 46  
... ... @@ -97,3 +114,8 @@ function showDiff(link) {
97 114 return _chosen.apply(this, [default_options]);
98 115 }})
99 116 })(jQuery);
  117 +
  118 +
  119 +function ajaxGet(url) {
  120 + $.ajax({type: "GET", url: url, dataType: "script"});
  121 +}
... ...
app/assets/javascripts/issues.js
... ... @@ -73,4 +73,25 @@ function issuesPage(){
73 73 $("#milestone_id, #assignee_id, #label_name").on("change", function(){
74 74 $(this).closest("form").submit();
75 75 });
  76 +
  77 + $('body').on('ajax:success', '.close_issue, .reopen_issue, #new_issue', function(){
  78 + var t = $(this),
  79 + totalIssues,
  80 + reopen = t.hasClass('reopen_issue'),
  81 + newIssue = false;
  82 + if( this.id == 'new_issue' ){
  83 + newIssue = true;
  84 + }
  85 + $('.issue_counter, #new_issue').each(function(){
  86 + var issue = $(this);
  87 + totalIssues = parseInt( $(this).html(), 10 );
  88 +
  89 + if( newIssue || ( reopen && issue.closest('.main_menu').length ) ){
  90 + $(this).html( totalIssues+1 );
  91 + }else {
  92 + $(this).html( totalIssues-1 );
  93 + }
  94 + });
  95 +
  96 + });
76 97 }
... ...
app/assets/javascripts/note.js
... ... @@ -25,11 +25,11 @@ init:
25 25 $(this).closest('li').fadeOut(); });
26 26  
27 27 $("#new_note").live("ajax:before", function(){
28   - $("#submit_note").attr("disabled", "disabled");
  28 + $(".submit_note").attr("disabled", "disabled");
29 29 })
30 30  
31 31 $("#new_note").live("ajax:complete", function(){
32   - $("#submit_note").removeAttr("disabled");
  32 + $(".submit_note").removeAttr("disabled");
33 33 })
34 34  
35 35 $("#note_note").live("focus", function(){
... ...
app/assets/stylesheets/common.scss
... ... @@ -604,7 +604,11 @@ li.note {
604 604 border-style: solid;
605 605 border-width: 1px;
606 606 @include border-radius(4px);
607   - min-height:42px;
  607 + min-height:22px;
  608 +
  609 + .avatar {
  610 + width:24px;
  611 + }
608 612 }
609 613  
610 614 .supp_diff_link,
... ...
app/assets/stylesheets/gitlab_bootstrap.scss
... ... @@ -202,6 +202,10 @@ a:focus {
202 202 color:$style_color;
203 203 }
204 204  
  205 +.nav-tabs > .active > a {
  206 + font-weight:bold;
  207 +}
  208 +
205 209 /** COLORS **/
206 210 .cgray { color:gray; }
207 211 .cred { color:#D12F19; }
... ... @@ -209,6 +213,7 @@ a:focus {
209 213 .cblack { color:#111; }
210 214 .cdark { color:#444 }
211 215 .cwhite { color:#fff !important }
  216 +.bgred { background: #F2DEDE !important}
212 217  
213 218 /** COMMON STYLES **/
214 219 .left {
... ... @@ -299,9 +304,24 @@ table.no-borders {
299 304 }
300 305  
301 306 .event_label {
302   - background: #FCEEC1;
303   - padding: 2px 2px 0;
304   - font-family: monospace;
  307 + @extend .label;
  308 + background-color: #999;
  309 +
  310 + &.pushed {
  311 + background-color: #3A87AD;
  312 + }
  313 +
  314 + &.opened {
  315 + background-color: #468847;
  316 + }
  317 +
  318 + &.closed {
  319 + background-color: #B94A48;
  320 + }
  321 +
  322 + &.merged {
  323 + background-color: #2A2;
  324 + }
305 325 }
306 326  
307 327 img.avatar {
... ... @@ -425,9 +445,10 @@ form {
425 445 */
426 446 .ui-box {
427 447 background:#F9F9F9;
428   - margin-bottom: 40px;
  448 + margin-bottom: 25px;
429 449 @include round-borders-all(4px);
430 450 border-color: #CCC;
  451 + @include solid_shade;
431 452  
432 453 ul {
433 454 margin:0;
... ... @@ -443,6 +464,13 @@ form {
443 464 background-image: -moz-linear-gradient(#eee 6.6%, #dfdfdf);
444 465 background-image: -o-linear-gradient(#eee 6.6%, #dfdfdf);
445 466  
  467 + &.small {
  468 + line-height: 28px;
  469 + font-size: 14px;
  470 + line-height:28px;
  471 + text-shadow: 0 1px 1px white;
  472 + }
  473 +
446 474 form {
447 475 padding:9px 0;
448 476 margin:0px;
... ... @@ -511,6 +539,7 @@ form {
511 539 table.admin-table {
512 540 @extend .table-bordered;
513 541 @extend .zebra-striped;
  542 + @include solid_shade;
514 543 th {
515 544 border-color: #CCC;
516 545 border-bottom: 1px solid #bbb;
... ... @@ -568,6 +597,8 @@ ul.breadcrumb {
568 597 @extend .prepend-top-20;
569 598 @extend .append-bottom-20;
570 599 border-width:1px;
  600 + @include solid_shade;
  601 +
571 602  
572 603 img { max-width: 100%; }
573 604  
... ... @@ -624,13 +655,166 @@ p {
624 655 h3.page_title {
625 656 color:#456;
626 657 font-size:20px;
627   - font-weight: 600;
  658 + font-weight: normal;
628 659 line-height: 28px;
629 660 }
630 661  
631   -pre.logs {
632   - .log {
633   - font-size:12px;
634   - line-height:18px;
  662 +/**
  663 + * File content holder
  664 + *
  665 + */
  666 +.file_holder {
  667 + border:1px solid #CCC;
  668 + margin-bottom:1em;
  669 + @include solid_shade;
  670 +
  671 + .file_title {
  672 + border-bottom: 1px solid #bbb;
  673 + background:#eee;
  674 + background-image: -webkit-gradient(linear, 0 0, 0 30, color-stop(0.066, #eee), to(#dfdfdf));
  675 + background-image: -webkit-linear-gradient(#eee 6.6%, #dfdfdf);
  676 + background-image: -moz-linear-gradient(#eee 6.6%, #dfdfdf);
  677 + background-image: -o-linear-gradient(#eee 6.6%, #dfdfdf);
  678 + margin: 0;
  679 + font-weight: normal;
  680 + font-weight: bold;
  681 + text-align: left;
  682 + color: #666;
  683 + padding: 9px 10px;
  684 + height:18px;
  685 +
  686 + .options {
  687 + float:right;
  688 + margin-top: -5px;
  689 + }
  690 +
  691 + .file_name {
  692 + color:$style_color;
  693 + font-size:14px;
  694 + text-shadow: 0 1px 1px #fff;
  695 + small {
  696 + color:#999;
  697 + font-size:13px;
  698 + }
  699 + }
  700 + }
  701 + .file_content {
  702 + background:#fff;
  703 + font-size: 11px;
  704 +
  705 + &.wiki {
  706 + font-size: 13px;
  707 + code {
  708 + padding:0 4px;
  709 + }
  710 + padding:20px;
  711 + h1, h2 {
  712 + line-height: 46px;
  713 + }
  714 + h3, h4 {
  715 + line-height: 40px;
  716 + }
  717 + }
  718 +
  719 + &.image_file {
  720 + background:#eee;
  721 + text-align:center;
  722 + img {
  723 + padding:100px;
  724 + max-width:300px;
  725 + }
  726 + }
  727 +
  728 + &.blob_file {
  729 +
  730 + }
  731 +
  732 + /**
  733 + * Blame file
  734 + */
  735 + &.blame {
  736 + tr {
  737 + border-bottom: 1px solid #eee;
  738 + }
  739 + td {
  740 + padding:5px;
  741 + }
  742 + .author,
  743 + .blame_commit {
  744 + background:#f5f5f5;
  745 + vertical-align:top;
  746 + }
  747 + .lines {
  748 + pre {
  749 + padding:0;
  750 + margin:0;
  751 + background:none;
  752 + border:none;
  753 + }
  754 + }
  755 + }
  756 +
  757 + &.logs {
  758 + background:#eee;
  759 + max-height: 700px;
  760 + overflow-y: auto;
  761 +
  762 + ol {
  763 + margin-left:40px;
  764 + padding: 10px 0;
  765 + border-left: 1px solid #CCC;
  766 + margin-bottom:0;
  767 + background: white;
  768 + li {
  769 + color:#888;
  770 + p {
  771 + margin:0;
  772 + color:#333;
  773 + line-height:24px;
  774 + padding-left: 10px;
  775 + }
  776 +
  777 + &:hover {
  778 + background:$hover;
  779 + }
  780 + }
  781 + }
  782 + }
  783 +
  784 + /**
  785 + * Code file
  786 + */
  787 + &.code {
  788 + padding:0;
  789 + td.code {
  790 + width: 100%;
  791 + .highlight {
  792 + margin-left: 55px;
  793 + overflow:auto;
  794 + overflow-y:hidden;
  795 + }
  796 + }
  797 + .highlight pre {
  798 + white-space: pre;
  799 + word-wrap:normal;
  800 + }
  801 +
  802 + table.highlighttable {
  803 + border: none;
  804 + }
  805 + body.project-page table.highlighttable td { border: none }
  806 + table.highlighttable tr:hover { background:none;}
  807 +
  808 + table.highlighttable pre{
  809 + line-height:16px !important;
  810 + font-size:12px !important;
  811 + }
  812 +
  813 + table.highlighttable .linenodiv pre {
  814 + text-align: right;
  815 + padding-right: 4px;
  816 + color:#666;
  817 + }
  818 + }
635 819 }
636 820 }
... ...
app/assets/stylesheets/header.scss
... ... @@ -96,7 +96,7 @@ header {
96 96 */
97 97 .search {
98 98 float: right;
99   - margin-right: 55px;
  99 + margin-right: 50px;
100 100  
101 101 .search-input {
102 102 @extend .span2;
... ... @@ -126,10 +126,10 @@ header {
126 126 cursor: pointer;
127 127 img {
128 128 border-radius: 4px;
129   - right: 0px;
  129 + right: 5px;
130 130 position: absolute;
131   - width: 33px;
132   - height: 33px;
  131 + width: 31px;
  132 + height: 31px;
133 133 display: block;
134 134 top: 0;
135 135 &:after {
... ...
app/assets/stylesheets/main.scss
... ... @@ -31,6 +31,12 @@ $hover: #FDF5D9;
31 31 box-shadow: 0 0 3px #ddd;
32 32 }
33 33  
  34 +@mixin solid_shade {
  35 + -moz-box-shadow: 0 0 0 3px #eee;
  36 + -webkit-box-shadow: 0 0 0 3px #eee;
  37 + box-shadow: 0 0 0 3px #eee;
  38 +}
  39 +
34 40 @mixin border-radius($radius) {
35 41 -moz-border-radius: $radius;
36 42 -webkit-border-radius: $radius;
... ... @@ -136,7 +142,7 @@ $hover: #FDF5D9;
136 142 /**
137 143 * Code (files list) styles. Browsing project files there
138 144 */
139   -@import "tree.scss";
  145 +@import "sections/tree.scss";
140 146  
141 147 /**
142 148 * This file represent notes(comments) styles
... ...
app/assets/stylesheets/notes.scss
... ... @@ -63,18 +63,22 @@ p.notify_controls span{
63 63  
64 64 tr.line_notes_row {
65 65 border-bottom:1px solid #DDD;
  66 + border-left: 7px solid #2A79A3;
  67 +
66 68 &.reply {
67 69 background:#eee;
68   -
  70 + border-left: 7px solid #2A79A3;
  71 + border-top:1px solid #ddd;
69 72 td {
70 73 padding:7px 10px;
71 74 }
72 75 a.line_note_reply_link {
73 76 @include round-borders-all(4px);
74   - border-color:#aaa;
75   - background: #bbb;
76   - padding: 3px 20px;
  77 + padding: 3px 10px;
  78 + margin-left:5px;
77 79 color: white;
  80 + background: #2A79A3;
  81 + border-color: #2A79A3;
78 82 }
79 83 }
80 84 ul {
... ... @@ -95,6 +99,9 @@ tr.line_notes_row {
95 99 td {
96 100 border-bottom:1px solid #ddd;
97 101 }
  102 + .actions {
  103 + margin:0;
  104 + }
98 105 }
99 106  
100 107 td .line_note_link {
... ...
app/assets/stylesheets/sections/commits.scss
... ... @@ -101,18 +101,21 @@
101 101 margin:50px;
102 102 padding:1px;
103 103 max-width:400px;
104   - }
105   - &.diff_image_removed {
106   - img {
  104 +
  105 + &.diff_image_removed {
107 106 border: 1px solid #C00;
108 107 }
109   - }
110 108  
111   - &.diff_image_added {
112   - img {
  109 + &.diff_image_added {
113 110 border: 1px solid #0C0;;
114 111 }
115 112 }
  113 +
  114 + &.img_compared {
  115 + img {
  116 + max-width:300px;
  117 + }
  118 + }
116 119 }
117 120 }
118 121  
... ...
app/assets/stylesheets/sections/merge_requests.scss
... ... @@ -82,3 +82,15 @@
82 82 }
83 83 }
84 84 }
  85 +
  86 +li.merge_request {
  87 + padding:7px 10px;
  88 + img.avatar {
  89 + width: 32px;
  90 + margin-top: 4px;
  91 + }
  92 + p {
  93 + padding: 0px;
  94 + padding-bottom: 2px;
  95 + }
  96 +}
... ...
app/assets/stylesheets/sections/tree.scss 0 → 100644
... ... @@ -0,0 +1,96 @@
  1 +#tree-holder {
  2 + #tree-content-holder {
  3 + float:left;
  4 + width:100%;
  5 + }
  6 + #tree-readme-holder {
  7 + float:left;
  8 + width:100%;
  9 + .readme {
  10 + border:1px solid #ccc;
  11 + padding:12px;
  12 + background: #F7F7F7;
  13 +
  14 + pre {
  15 + overflow: auto;
  16 + }
  17 + }
  18 + }
  19 +
  20 + .tree_progress {
  21 + display:none;
  22 + margin:20px;
  23 + &.loading {
  24 + display:block;
  25 + }
  26 + }
  27 +
  28 + #tree-slider {
  29 + @include border-radius(0);
  30 + .tree-item {
  31 + &:hover {
  32 + td { background: $hover; }
  33 + cursor:pointer;
  34 + }
  35 + }
  36 + }
  37 +
  38 + .tree-item {
  39 + .tree-item-file-name {
  40 + vertical-align:middle;
  41 + font-weight:bold;
  42 + a {
  43 + color:$style_color;
  44 + &:hover {
  45 + color:$blue_link;
  46 + }
  47 + }
  48 +
  49 + img {
  50 + position: relative;
  51 + top:-1px;
  52 + }
  53 + }
  54 + }
  55 +
  56 +
  57 + #tree-slider {
  58 + @include solid_shade;
  59 + width:100%;
  60 +
  61 + border-color:#ccc;
  62 +
  63 + td {
  64 + padding:8px;
  65 + border-color:#f1f1f1;
  66 + background:#fafafa;
  67 + }
  68 +
  69 + tr:first-child td:first-child,
  70 + tr:first-child td:last-child {
  71 + border-radius:0;
  72 + }
  73 +
  74 + th {
  75 + border-color: #CCC;
  76 + border-bottom: 1px solid #bbb;
  77 + background:#eee;
  78 + background-image: -webkit-gradient(linear, 0 0, 0 30, color-stop(0.066, #eee), to(#dfdfdf));
  79 + background-image: -webkit-linear-gradient(#eee 6.6%, #dfdfdf);
  80 + background-image: -moz-linear-gradient(#eee 6.6%, #dfdfdf);
  81 + background-image: -o-linear-gradient(#eee 6.6%, #dfdfdf);
  82 + }
  83 + }
  84 +
  85 + .tree-commit-link {
  86 + color:#333;
  87 + }
  88 +
  89 + a.tree-commit-link {
  90 + color: #666;
  91 + &:hover {
  92 + text-decoration: underline;
  93 + }
  94 + }
  95 +
  96 +}
... ...
app/assets/stylesheets/themes/ui_mars.scss
... ... @@ -70,8 +70,7 @@
70 70 }
71 71 }
72 72 .separator {
73   - border-color:#444;
74   - background:#31363E;
  73 + display:none;
75 74 }
76 75  
77 76 }
... ...
app/assets/stylesheets/tree.scss
... ... @@ -1,232 +0,0 @@
1   -#tree-holder {
2   - #tree-content-holder {
3   - float:left;
4   - width:100%;
5   - }
6   - #tree-readme-holder {
7   - float:left;
8   - width:100%;
9   - .readme {
10   - border:1px solid #ccc;
11   - padding:12px;
12   - background: #F7F7F7;
13   -
14   - pre {
15   - overflow: auto;
16   - }
17   - }
18   - }
19   -
20   - .tree_progress {
21   - display:none;
22   - margin:20px;
23   - &.loading {
24   - display:block;
25   - }
26   - }
27   -
28   -
29   - /** FILE CONTENT VIEW **/
30   - .view_file_content{
31   - .old_line, .new_line {
32   - background:#ECECEC;
33   - color:#777;
34   - width:15px;
35   - float:left;
36   - padding: 0px 10px;
37   - border-right: 1px solid #ccc;
38   - }
39   - .old_line{
40   - display:none;
41   - }
42   - }
43   -
44   - .view_file .view_file_header,
45   - .diff_file .diff_file_header {
46   - border-bottom: 1px solid #bbb;
47   - background:#eee;
48   - background-image: -webkit-gradient(linear, 0 0, 0 30, color-stop(0.066, #eee), to(#dfdfdf));
49   - background-image: -webkit-linear-gradient(#eee 6.6%, #dfdfdf);
50   - background-image: -moz-linear-gradient(#eee 6.6%, #dfdfdf);
51   - background-image: -o-linear-gradient(#eee 6.6%, #dfdfdf);
52   - margin: 0;
53   - font-weight: normal;
54   - font-weight: bold;
55   - text-align: left;
56   - color: #666;
57   - padding: 9px 10px;
58   - height:18px;
59   -
60   - .options {
61   - float:right;
62   - margin-top: -5px;
63   - }
64   -
65   - .file_name {
66   - color:$style_color;
67   - font-size:14px;
68   - text-shadow: 0 1px 1px #fff;
69   - small {
70   - color:#999;
71   - font-size:13px;
72   - }
73   - }
74   - }
75   -
76   - .view_file {
77   - border:1px solid #CCC;
78   - margin-bottom:1em;
79   -
80   - .view_file_content {
81   - background:#fff;
82   - color:#514721;
83   - font-size: 11px;
84   - }
85   - .view_file_content_image {
86   - background:#eee;
87   - text-align:center;
88   - img {
89   - padding:100px;
90   - max-width:300px;
91   - }
92   - }
93   - }
94   -
95   - td.code {
96   - width: 100%;
97   - .highlight {
98   - margin-left: 55px;
99   - overflow:auto;
100   - overflow-y:hidden;
101   - }
102   - }
103   - .highlight pre {
104   - white-space: pre;
105   - word-wrap:normal;
106   - }
107   -
108   - table.highlighttable {
109   - border: none;
110   - }
111   - body.project-page table.highlighttable td { border: none }
112   - table.highlighttable tr:hover { background:none;}
113   -
114   - table.highlighttable pre{
115   - line-height:16px !important;
116   - font-size:12px !important;
117   - }
118   -
119   - table.highlighttable .linenodiv pre {
120   - text-align: right;
121   - padding-right: 4px;
122   - color:#666;
123   - }
124   -
125   - #tree-slider {
126   - @include border-radius(0);
127   - .tree-item {
128   - &:hover {
129   - td { background: $hover; }
130   - cursor:pointer;
131   - }
132   - }
133   - }
134   -
135   - .tree-item {
136   - .tree-item-file-name {
137   - vertical-align:middle;
138   - font-weight:bold;
139   - a {
140   - color:$style_color;
141   - &:hover {
142   - color:$blue_link;
143   - }
144   - }
145   -
146   - img {
147   - position: relative;
148   - top:-1px;
149   - }
150   - }
151   - }
152   -
153   -
154   - #tree-slider {
155   - @include shade;
156   - width:100%;
157   -
158   - border-color:#ccc;
159   -
160   - td {
161   - padding:8px;
162   - border-color:#f1f1f1;
163   - background:#fafafa;
164   - }
165   -
166   - tr:first-child td:first-child,
167   - tr:first-child td:last-child {
168   - border-radius:0;
169   - }
170   -
171   - th {
172   - border-color: #CCC;
173   - border-bottom: 1px solid #bbb;
174   - background:#eee;
175   - background-image: -webkit-gradient(linear, 0 0, 0 30, color-stop(0.066, #eee), to(#dfdfdf));
176   - background-image: -webkit-linear-gradient(#eee 6.6%, #dfdfdf);
177   - background-image: -moz-linear-gradient(#eee 6.6%, #dfdfdf);
178   - background-image: -o-linear-gradient(#eee 6.6%, #dfdfdf);
179   - }
180   - }
181   -
182   - .tree-commit-link {
183   - color:#333;
184   - }
185   -
186   - #tree-content-holder .view_file{
187   - @include shade;
188   - }
189   -
190   - #tree-readme-holder .readme {
191   - @include shade;
192   - margin-bottom:20px;
193   - h1, h2 {
194   - line-height: 56px;
195   - }
196   - h3, h4 {
197   - line-height: 46px;
198   - }
199   - }
200   -
201   - a.tree-commit-link {
202   - color: #666;
203   - &:hover {
204   - text-decoration: underline;
205   - }
206   - }
207   -
208   -}
209   -
210   -.blame_file {
211   - .view_file_content {
212   - tr {
213   - border-bottom: 1px solid #eee;
214   - }
215   - td {
216   - padding:5px;
217   - }
218   - .author,
219   - .commit {
220   - background:#f5f5f5;
221   - vertical-align:top;
222   - }
223   - .lines {
224   - pre {
225   - padding:0;
226   - margin:0;
227   - background:none;
228   - border:none;
229   - }
230   - }
231   - }
232   -}
app/contexts/base_context.rb 0 → 100644
... ... @@ -0,0 +1,8 @@
  1 +class BaseContext
  2 + attr_accessor :project, :current_user, :params
  3 +
  4 + def initialize(project, user, params)
  5 + @project, @current_user, @params = project, user, params.dup
  6 + end
  7 +end
  8 +
... ...
app/contexts/commit_load.rb 0 → 100644
... ... @@ -0,0 +1,26 @@
  1 +class CommitLoad < BaseContext
  2 + def execute
  3 + result = {
  4 + :commit => nil,
  5 + :suppress_diff => false,
  6 + :line_notes => [],
  7 + :notes_count => 0,
  8 + :note => nil
  9 + }
  10 +
  11 + commit = project.commit(params[:id])
  12 +
  13 + if commit
  14 + commit = CommitDecorator.decorate(commit)
  15 + line_notes = project.commit_line_notes(commit)
  16 +
  17 + result[:suppress_diff] = true if commit.diffs.size > 200 && !params[:force_show_diff]
  18 + result[:commit] = commit
  19 + result[:note] = project.build_commit_note(commit)
  20 + result[:line_notes] = line_notes
  21 + result[:notes_count] = line_notes.count + project.commit_notes(commit).count
  22 + end
  23 +
  24 + result
  25 + end
  26 +end
... ...
app/contexts/merge_requests_load.rb 0 → 100644
... ... @@ -0,0 +1,16 @@
  1 +class MergeRequestsLoad < BaseContext
  2 + def execute
  3 + type = params[:f].to_i
  4 +
  5 + merge_requests = project.merge_requests
  6 +
  7 + merge_requests = case type
  8 + when 1 then merge_requests
  9 + when 2 then merge_requests.closed
  10 + when 3 then merge_requests.opened.assigned(current_user)
  11 + else merge_requests.opened
  12 + end.page(params[:page]).per(20)
  13 +
  14 + merge_requests.includes(:author, :project).order("closed, created_at desc")
  15 + end
  16 +end
... ...
app/contexts/notes_load.rb 0 → 100644
... ... @@ -0,0 +1,30 @@
  1 +class NotesLoad < BaseContext
  2 + def execute
  3 + target_type = params[:target_type]
  4 + target_id = params[:target_id]
  5 + first_id = params[:first_id]
  6 + last_id = params[:last_id]
  7 +
  8 +
  9 + @notes = case target_type
  10 + when "commit"
  11 + then project.commit_notes(project.commit(target_id)).fresh.limit(20)
  12 + when "snippet"
  13 + then project.snippets.find(target_id).notes
  14 + when "wall"
  15 + then project.common_notes.order("created_at DESC").fresh.limit(50)
  16 + when "issue"
  17 + then project.issues.find(target_id).notes.inc_author.order("created_at DESC").limit(20)
  18 + when "merge_request"
  19 + then project.merge_requests.find(target_id).notes.inc_author.order("created_at DESC").limit(20)
  20 + end
  21 +
  22 + @notes = if last_id
  23 + @notes.where("id > ?", last_id)
  24 + elsif first_id
  25 + @notes.where("id < ?", first_id)
  26 + else
  27 + @notes
  28 + end
  29 + end
  30 +end
... ...
app/controllers/admin/hooks_controller.rb 0 → 100644
... ... @@ -0,0 +1,44 @@
  1 +class Admin::HooksController < ApplicationController
  2 + layout "admin"
  3 + before_filter :authenticate_user!
  4 + before_filter :authenticate_admin!
  5 +
  6 + def index
  7 + @hooks = SystemHook.all
  8 + @hook = SystemHook.new
  9 + end
  10 +
  11 + def create
  12 + @hook = SystemHook.new(params[:hook])
  13 +
  14 + if @hook.save
  15 + redirect_to admin_hooks_path, notice: 'Hook was successfully created.'
  16 + else
  17 + @hooks = SystemHook.all
  18 + render :index
  19 + end
  20 + end
  21 +
  22 + def destroy
  23 + @hook = SystemHook.find(params[:id])
  24 + @hook.destroy
  25 +
  26 + redirect_to admin_hooks_path
  27 + end
  28 +
  29 +
  30 + def test
  31 + @hook = SystemHook.find(params[:hook_id])
  32 + data = {
  33 + event_name: "project_create",
  34 + name: "Ruby",
  35 + path: "ruby",
  36 + project_id: 1,
  37 + owner_name: "Someone",
  38 + owner_email: "example@gitlabhq.com"
  39 + }
  40 + @hook.execute(data)
  41 +
  42 + redirect_to :back
  43 + end
  44 +end
... ...
app/controllers/admin/mailer_controller.rb
... ... @@ -1,45 +0,0 @@
1   -class Admin::MailerController < ApplicationController
2   - layout "admin"
3   - before_filter :authenticate_user!
4   - before_filter :authenticate_admin!
5   -
6   - def preview
7   -
8   - end
9   -
10   - def preview_note
11   - @note = Note.first
12   - @user = @note.author
13   - @project = @note.project
14   - case params[:type]
15   - when "Commit" then
16   - @commit = @project.commit
17   - render :file => 'notify/note_commit_email', :layout => 'notify'
18   - when "Issue" then
19   - @issue = Issue.first
20   - render :file => 'notify/note_issue_email', :layout => 'notify'
21   - else
22   - render :file => 'notify/note_wall_email', :layout => 'notify'
23   - end
24   - rescue
25   - render :text => "Preview not available"
26   - end
27   -
28   - def preview_user_new
29   - @user = User.first
30   - @password = "DHasJKDHAS!"
31   -
32   - render :file => 'notify/new_user_email', :layout => 'notify'
33   - rescue
34   - render :text => "Preview not available"
35   - end
36   -
37   - def preview_issue_new
38   - @issue = Issue.first
39   - @user = @issue.assignee
40   - @project = @issue.project
41   - render :file => 'notify/new_issue_email', :layout => 'notify'
42   - rescue
43   - render :text => "Preview not available"
44   - end
45   -end
app/controllers/admin/projects_controller.rb
... ... @@ -6,7 +6,7 @@ class Admin::ProjectsController &lt; ApplicationController
6 6 def index
7 7 @admin_projects = Project.scoped
8 8 @admin_projects = @admin_projects.search(params[:name]) if params[:name].present?
9   - @admin_projects = @admin_projects.page(params[:page])
  9 + @admin_projects = @admin_projects.page(params[:page]).per(20)
10 10 end
11 11  
12 12 def show
... ... @@ -72,6 +72,6 @@ class Admin::ProjectsController &lt; ApplicationController
72 72 @admin_project = Project.find_by_code(params[:id])
73 73 @admin_project.destroy
74 74  
75   - redirect_to admin_projects_url
  75 + redirect_to admin_projects_url, notice: 'Project was successfully deleted.'
76 76 end
77 77 end
... ...
app/controllers/application_controller.rb
... ... @@ -52,7 +52,7 @@ class ApplicationController &lt; ActionController::Base
52 52  
53 53 def layout_by_resource
54 54 if devise_controller?
55   - "devise"
  55 + "devise_layout"
56 56 else
57 57 "application"
58 58 end
... ...
app/controllers/commits_controller.rb
... ... @@ -26,43 +26,31 @@ class CommitsController &lt; ApplicationController
26 26 end
27 27  
28 28 def show
29   - @commit = project.commit(params[:id])
30   -
31   - git_not_found! and return unless @commit
32   -
33   - @commit = CommitDecorator.decorate(@commit)
34   -
35   - @note = @project.build_commit_note(@commit)
36   - @comments_allowed = true
37   - @line_notes = project.commit_line_notes(@commit)
38   -
39   - @notes_count = @line_notes.count + project.commit_notes(@commit).count
40   -
41   - if @commit.diffs.size > 200 && !params[:force_show_diff]
42   - @suppress_diff = true
  29 + result = CommitLoad.new(project, current_user, params).execute
  30 +
  31 + @commit = result[:commit]
  32 +
  33 + if @commit
  34 + @suppress_diff = result[:suppress_diff]
  35 + @note = result[:note]
  36 + @line_notes = result[:line_notes]
  37 + @notes_count = result[:notes_count]
  38 + @comments_allowed = true
  39 + else
  40 + return git_not_found!
43 41 end
  42 +
44 43 rescue Grit::Git::GitTimeout
45 44 render "huge_commit"
46 45 end
47 46  
48 47 def compare
49   - first = project.commit(params[:to].try(:strip))
50   - last = project.commit(params[:from].try(:strip))
  48 + result = Commit.compare(project, params[:from], params[:to])
51 49  
52   - @diffs = []
53   - @commits = []
  50 + @commits = result[:commits]
  51 + @commit = result[:commit]
  52 + @diffs = result[:diffs]
54 53 @line_notes = []
55   -
56   - if first && last
57   - commits = [first, last].sort_by(&:created_at)
58   - younger = commits.first
59   - older = commits.last
60   -
61   -
62   - @commits = project.repo.commits_between(younger.id, older.id).map {|c| Commit.new(c)}
63   - @diffs = project.repo.diff(younger.id, older.id) rescue []
64   - @commit = Commit.new(older)
65   - end
66 54 end
67 55  
68 56 def patch
... ...
app/controllers/dashboard_controller.rb
... ... @@ -2,15 +2,13 @@ class DashboardController &lt; ApplicationController
2 2 respond_to :html
3 3  
4 4 def index
5   - @projects = current_user.projects.includes(:events).order("events.created_at DESC")
6   - @projects = @projects.page(params[:page]).per(40)
7   -
8   - @events = Event.where(:project_id => current_user.projects.map(&:id)).recent.limit(20)
9   -
  5 + @projects = current_user.projects_with_events.page(params[:page]).per(40)
  6 + @events = Event.recent_for_user(current_user).limit(20).offset(params[:offset] || 0)
10 7 @last_push = current_user.recent_push
11 8  
12 9 respond_to do |format|
13 10 format.html
  11 + format.js
14 12 format.atom { render :layout => false }
15 13 end
16 14 end
... ...
app/controllers/hooks_controller.rb
... ... @@ -11,24 +11,24 @@ class HooksController &lt; ApplicationController
11 11 respond_to :html
12 12  
13 13 def index
14   - @hooks = @project.web_hooks.all
15   - @hook = WebHook.new
  14 + @hooks = @project.hooks.all
  15 + @hook = ProjectHook.new
16 16 end
17 17  
18 18 def create
19   - @hook = @project.web_hooks.new(params[:hook])
  19 + @hook = @project.hooks.new(params[:hook])
20 20 @hook.save
21 21  
22 22 if @hook.valid?
23 23 redirect_to project_hooks_path(@project)
24 24 else
25   - @hooks = @project.web_hooks.all
  25 + @hooks = @project.hooks.all
26 26 render :index
27 27 end
28 28 end
29 29  
30 30 def test
31   - @hook = @project.web_hooks.find(params[:id])
  31 + @hook = @project.hooks.find(params[:id])
32 32 commits = @project.commits(@project.default_branch, nil, 3)
33 33 data = @project.post_receive_data(commits.last.id, commits.first.id, "refs/heads/#{@project.default_branch}", current_user)
34 34 @hook.execute(data)
... ... @@ -37,7 +37,7 @@ class HooksController &lt; ApplicationController
37 37 end
38 38  
39 39 def destroy
40   - @hook = @project.web_hooks.find(params[:id])
  40 + @hook = @project.hooks.find(params[:id])
41 41 @hook.destroy
42 42  
43 43 redirect_to project_hooks_path(@project)
... ...
app/controllers/merge_requests_controller.rb
... ... @@ -24,16 +24,7 @@ class MergeRequestsController &lt; ApplicationController
24 24  
25 25  
26 26 def index
27   - @merge_requests = @project.merge_requests
28   -
29   - @merge_requests = case params[:f].to_i
30   - when 1 then @merge_requests
31   - when 2 then @merge_requests.closed
32   - when 3 then @merge_requests.opened.assigned(current_user)
33   - else @merge_requests.opened
34   - end.page(params[:page]).per(20)
35   -
36   - @merge_requests = @merge_requests.includes(:author, :project).order("closed, created_at desc")
  27 + @merge_requests = MergeRequestsLoad.new(project, current_user, params).execute
37 28 end
38 29  
39 30 def show
... ...
app/controllers/notes_controller.rb
... ... @@ -40,25 +40,6 @@ class NotesController &lt; ApplicationController
40 40 protected
41 41  
42 42 def notes
43   - @notes = case params[:target_type]
44   - when "commit"
45   - then project.commit_notes(project.commit((params[:target_id]))).fresh.limit(20)
46   - when "snippet"
47   - then project.snippets.find(params[:target_id]).notes
48   - when "wall"
49   - then project.common_notes.order("created_at DESC").fresh.limit(50)
50   - when "issue"
51   - then project.issues.find(params[:target_id]).notes.inc_author.order("created_at DESC").limit(20)
52   - when "merge_request"
53   - then project.merge_requests.find(params[:target_id]).notes.inc_author.order("created_at DESC").limit(20)
54   - end
55   -
56   - @notes = if params[:last_id]
57   - @notes.where("id > ?", params[:last_id])
58   - elsif params[:first_id]
59   - @notes.where("id < ?", params[:first_id])
60   - else
61   - @notes
62   - end
  43 + @notes = NotesLoad.new(project, current_user, params).execute
63 44 end
64 45 end
... ...
app/controllers/omniauth_callbacks_controller.rb
1 1 class OmniauthCallbacksController < Devise::OmniauthCallbacksController
  2 +
  3 + # Extend the standard message generation to accept our custom exception
  4 + def failure_message
  5 + exception = env["omniauth.error"]
  6 + if exception.class == OmniAuth::Error
  7 + error = exception.message
  8 + else
  9 + error = exception.error_reason if exception.respond_to?(:error_reason)
  10 + error ||= exception.error if exception.respond_to?(:error)
  11 + error ||= env["omniauth.error.type"].to_s
  12 + end
  13 + error.to_s.humanize if error
  14 + end
2 15  
3 16 def ldap
4 17 # We only find ourselves here if the authentication to LDAP was successful.
... ...
app/controllers/refs_controller.rb
... ... @@ -9,7 +9,7 @@ class RefsController &lt; ApplicationController
9 9 before_filter :require_non_empty_project
10 10  
11 11 before_filter :ref
12   - before_filter :define_tree_vars, :only => [:tree, :blob, :blame]
  12 + before_filter :define_tree_vars, :only => [:tree, :blob, :blame, :logs_tree]
13 13 before_filter :render_full_content
14 14  
15 15 layout "project"
... ... @@ -46,6 +46,18 @@ class RefsController &lt; ApplicationController
46 46 end
47 47 end
48 48  
  49 + def logs_tree
  50 + contents = @tree.contents
  51 + @logs = contents.map do |content|
  52 + file = params[:path] ? File.join(params[:path], content.name) : content.name
  53 + last_commit = @project.commits(@commit.id, file, 1).last
  54 + {
  55 + :file_name => content.name,
  56 + :commit => last_commit
  57 + }
  58 + end
  59 + end
  60 +
49 61 def blob
50 62 if @tree.is_blob?
51 63 if @tree.text?
... ... @@ -79,6 +91,15 @@ class RefsController &lt; ApplicationController
79 91 @commit = project.commit(@ref)
80 92 @tree = Tree.new(@commit.tree, project, @ref, params[:path])
81 93 @tree = TreeDecorator.new(@tree)
  94 + @hex_path = Digest::SHA1.hexdigest(params[:path] || "/")
  95 +
  96 + if params[:path]
  97 + @history_path = tree_file_project_ref_path(@project, @ref, params[:path])
  98 + @logs_path = logs_file_project_ref_path(@project, @ref, params[:path])
  99 + else
  100 + @history_path = tree_project_ref_path(@project, @ref)
  101 + @logs_path = logs_tree_project_ref_path(@project, @ref)
  102 + end
82 103 rescue
83 104 return render_404
84 105 end
... ...
app/decorators/event_decorator.rb 0 → 100644
... ... @@ -0,0 +1,25 @@
  1 +class EventDecorator < ApplicationDecorator
  2 + decorates :event
  3 +
  4 + def feed_title
  5 + if self.issue?
  6 + "#{self.author_name} #{self.action_name} issue ##{self.target_id}:" + self.issue_title
  7 + elsif self.merge_request?
  8 + "#{self.author_name} #{self.action_name} MR ##{self.target_id}:" + self.merge_request_title
  9 + elsif self.push?
  10 + "#{self.author_name} #{self.push_action_name} #{self.ref_type} " + self.ref_name
  11 + else
  12 + ""
  13 + end
  14 + end
  15 +
  16 + def feed_url
  17 + if self.issue?
  18 + h.project_issue_url(self.project, self.issue)
  19 + elsif self.merge_request?
  20 + h.project_merge_request_url(self.project, self.merge_request)
  21 + elsif self.push?
  22 + h.project_commits_url(self.project, :ref => self.ref_name)
  23 + end
  24 + end
  25 +end
... ...
app/helpers/application_helper.rb
... ... @@ -134,4 +134,8 @@ module ApplicationHelper
134 134 end
135 135 active ? "current" : nil
136 136 end
  137 +
  138 + def hexdigest(string)
  139 + Digest::SHA1.hexdigest string
  140 + end
137 141 end
... ...
app/helpers/tree_helper.rb 0 → 100644
... ... @@ -0,0 +1,27 @@
  1 +module TreeHelper
  2 + def tree_icon(content)
  3 + if content.is_a?(Grit::Blob)
  4 + if content.text?
  5 + image_tag "file_txt.png"
  6 + elsif content.image?
  7 + image_tag "file_img.png"
  8 + else
  9 + image_tag "file_bin.png"
  10 + end
  11 + else
  12 + image_tag "file_dir.png"
  13 + end
  14 + end
  15 +
  16 + def tree_hex_class(content)
  17 + "file_#{hexdigest(content.name)}"
  18 + end
  19 +
  20 + def tree_full_path(content)
  21 + if params[:path]
  22 + File.join(params[:path], content.name)
  23 + else
  24 + content.name
  25 + end
  26 + end
  27 +end
... ...
app/models/commit.rb
... ... @@ -80,6 +80,29 @@ class Commit
80 80 def commits_between(repo, from, to)
81 81 repo.commits_between(from, to).map { |c| Commit.new(c) }
82 82 end
  83 +
  84 + def compare(project, from, to)
  85 + first = project.commit(to.try(:strip))
  86 + last = project.commit(from.try(:strip))
  87 +
  88 + result = {
  89 + :commits => [],
  90 + :diffs => [],
  91 + :commit => nil
  92 + }
  93 +
  94 + if first && last
  95 + commits = [first, last].sort_by(&:created_at)
  96 + younger = commits.first
  97 + older = commits.last
  98 +
  99 + result[:commits] = project.repo.commits_between(younger.id, older.id).map {|c| Commit.new(c)}
  100 + result[:diffs] = project.repo.diff(younger.id, older.id) rescue []
  101 + result[:commit] = Commit.new(older)
  102 + end
  103 +
  104 + result
  105 + end
83 106 end
84 107  
85 108 def persisted?
... ...
app/models/event.rb
... ... @@ -28,6 +28,10 @@ class Event &lt; ActiveRecord::Base
28 28 end
29 29 end
30 30  
  31 + def self.recent_for_user user
  32 + where(:project_id => user.projects.map(&:id)).recent
  33 + end
  34 +
31 35 # Next events currently enabled for system
32 36 # - push
33 37 # - new issue
... ...
app/models/merge_request.rb
... ... @@ -22,7 +22,6 @@ class MergeRequest &lt; ActiveRecord::Base
22 22 :should_remove_source_branch
23 23  
24 24 validates_presence_of :project_id
25   - validates_presence_of :assignee_id
26 25 validates_presence_of :author_id
27 26 validates_presence_of :source_branch
28 27 validates_presence_of :target_branch
... ... @@ -36,6 +35,7 @@ class MergeRequest &lt; ActiveRecord::Base
36 35 delegate :name,
37 36 :email,
38 37 :to => :assignee,
  38 + :allow_nil => true,
39 39 :prefix => true
40 40  
41 41 validates :title,
... ... @@ -128,7 +128,7 @@ class MergeRequest &lt; ActiveRecord::Base
128 128  
129 129 def unmerged_diffs
130 130 commits = project.repo.commits_between(target_branch, source_branch).map {|c| Commit.new(c)}
131   - diffs = project.repo.diff(commits.first.prev_commit.id, commits.last.id)
  131 + diffs = project.repo.diff(commits.first.prev_commit.id, commits.last.id) rescue []
132 132 end
133 133  
134 134 def last_commit
... ...
app/models/project.rb
... ... @@ -19,7 +19,7 @@ class Project &lt; ActiveRecord::Base
19 19 has_many :notes, :dependent => :destroy
20 20 has_many :snippets, :dependent => :destroy
21 21 has_many :deploy_keys, :dependent => :destroy, :foreign_key => "project_id", :class_name => "Key"
22   - has_many :web_hooks, :dependent => :destroy
  22 + has_many :hooks, :dependent => :destroy, :class_name => "ProjectHook"
23 23 has_many :wikis, :dependent => :destroy
24 24 has_many :protected_branches, :dependent => :destroy
25 25  
... ... @@ -120,7 +120,7 @@ class Project &lt; ActiveRecord::Base
120 120 errors.add(:path, " like 'gitolite-admin' is not allowed")
121 121 end
122 122 end
123   -
  123 +
124 124 def self.access_options
125 125 UsersProject.access_roles
126 126 end
... ...
app/models/project_hook.rb 0 → 100644
... ... @@ -0,0 +1,3 @@
  1 +class ProjectHook < WebHook
  2 + belongs_to :project
  3 +end
... ...
app/models/system_hook.rb 0 → 100644
... ... @@ -0,0 +1,13 @@
  1 +class SystemHook < WebHook
  2 +
  3 + def async_execute(data)
  4 + Resque.enqueue(SystemHookWorker, id, data)
  5 + end
  6 +
  7 + def self.all_hooks_fire(data)
  8 + SystemHook.all.each do |sh|
  9 + sh.async_execute data
  10 + end
  11 + end
  12 +
  13 +end
... ...
app/models/user.rb
1 1 class User < ActiveRecord::Base
  2 +
2 3 include Account
3 4  
4   - devise :database_authenticatable, :token_authenticatable,
  5 + devise :database_authenticatable, :token_authenticatable, :lockable,
5 6 :recoverable, :rememberable, :trackable, :validatable, :omniauthable
6 7  
7 8 attr_accessible :email, :password, :password_confirmation, :remember_me, :bio,
8   - :name, :projects_limit, :skype, :linkedin, :twitter, :dark_scheme,
  9 + :name, :projects_limit, :skype, :linkedin, :twitter, :dark_scheme,
9 10 :theme_id, :force_random_password
10 11  
11 12 attr_accessor :force_random_password
... ... @@ -15,6 +16,11 @@ class User &lt; ActiveRecord::Base
15 16 has_many :my_own_projects, :class_name => "Project", :foreign_key => :owner_id
16 17 has_many :keys, :dependent => :destroy
17 18  
  19 + has_many :events,
  20 + :class_name => "Event",
  21 + :foreign_key => :author_id,
  22 + :dependent => :destroy
  23 +
18 24 has_many :recent_events,
19 25 :class_name => "Event",
20 26 :foreign_key => :author_id,
... ... @@ -80,7 +86,8 @@ class User &lt; ActiveRecord::Base
80 86  
81 87 def self.find_for_ldap_auth(omniauth_info)
82 88 name = omniauth_info.name.force_encoding("utf-8")
83   - email = omniauth_info.email.downcase
  89 + email = omniauth_info.email.downcase unless omniauth_info.email.nil?
  90 + raise OmniAuth::Error, "LDAP accounts must provide an email address" if email.nil?
84 91  
85 92 if @user = User.find_by_email(email)
86 93 @user
... ...
app/models/users_project.rb
... ... @@ -68,7 +68,7 @@ class UsersProject &lt; ActiveRecord::Base
68 68 end
69 69  
70 70 def repo_access_human
71   - ""
  71 + self.class.access_roles.invert[self.project_access]
72 72 end
73 73 end
74 74 # == Schema Information
... ...
app/models/web_hook.rb
... ... @@ -4,8 +4,6 @@ class WebHook &lt; ActiveRecord::Base
4 4 # HTTParty timeout
5 5 default_timeout 10
6 6  
7   - belongs_to :project
8   -
9 7 validates :url,
10 8 presence: true,
11 9 format: {
... ... @@ -14,9 +12,8 @@ class WebHook &lt; ActiveRecord::Base
14 12  
15 13 def execute(data)
16 14 WebHook.post(url, body: data.to_json, headers: { "Content-Type" => "application/json" })
17   - rescue
18   - # There was a problem calling this web hook, let's forget about it.
19 15 end
  16 +
20 17 end
21 18 # == Schema Information
22 19 #
... ...
app/observers/mailer_observer.rb
... ... @@ -43,7 +43,7 @@ class MailerObserver &lt; ActiveRecord::Observer
43 43 end
44 44  
45 45 def new_merge_request(merge_request)
46   - if merge_request.assignee != current_user
  46 + if merge_request.assignee && merge_request.assignee != current_user
47 47 Notify.new_merge_request_email(merge_request.id).deliver
48 48 end
49 49 end
... ...
app/observers/system_hook_observer.rb 0 → 100644
... ... @@ -0,0 +1,67 @@
  1 +class SystemHookObserver < ActiveRecord::Observer
  2 + observe :user, :project, :users_project
  3 +
  4 + def after_create(model)
  5 + if model.kind_of? Project
  6 + SystemHook.all_hooks_fire({
  7 + event_name: "project_create",
  8 + name: model.name,
  9 + path: model.path,
  10 + project_id: model.id,
  11 + owner_name: model.owner.name,
  12 + owner_email: model.owner.email,
  13 + created_at: model.created_at
  14 + })
  15 + elsif model.kind_of? User
  16 + SystemHook.all_hooks_fire({
  17 + event_name: "user_create",
  18 + name: model.name,
  19 + email: model.email,
  20 + created_at: model.created_at
  21 + })
  22 +
  23 + elsif model.kind_of? UsersProject
  24 + SystemHook.all_hooks_fire({
  25 + event_name: "user_add_to_team",
  26 + project_name: model.project.name,
  27 + project_path: model.project.path,
  28 + project_id: model.project_id,
  29 + user_name: model.user.name,
  30 + user_email: model.user.email,
  31 + project_access: model.repo_access_human,
  32 + created_at: model.created_at
  33 + })
  34 +
  35 + end
  36 + end
  37 +
  38 + def after_destroy(model)
  39 + if model.kind_of? Project
  40 + SystemHook.all_hooks_fire({
  41 + event_name: "project_destroy",
  42 + name: model.name,
  43 + path: model.path,
  44 + project_id: model.id,
  45 + owner_name: model.owner.name,
  46 + owner_email: model.owner.email,
  47 + })
  48 + elsif model.kind_of? User
  49 + SystemHook.all_hooks_fire({
  50 + event_name: "user_destroy",
  51 + name: model.name,
  52 + email: model.email
  53 + })
  54 +
  55 + elsif model.kind_of? UsersProject
  56 + SystemHook.all_hooks_fire({
  57 + event_name: "user_remove_from_team",
  58 + project_name: model.project.name,
  59 + project_path: model.project.path,
  60 + project_id: model.project_id,
  61 + user_name: model.user.name,
  62 + user_email: model.user.email,
  63 + project_access: model.repo_access_human
  64 + })
  65 + end
  66 + end
  67 +end
... ...
app/roles/account.rb
... ... @@ -55,4 +55,8 @@ module Account
55 55 # Take only latest one
56 56 events = events.recent.limit(1).first
57 57 end
  58 +
  59 + def projects_with_events
  60 + projects.includes(:events).order("events.created_at DESC")
  61 + end
58 62 end
... ...
app/roles/git_push.rb
... ... @@ -27,7 +27,7 @@ module GitPush
27 27 true
28 28 end
29 29  
30   - def execute_web_hooks(oldrev, newrev, ref, user)
  30 + def execute_hooks(oldrev, newrev, ref, user)
31 31 ref_parts = ref.split('/')
32 32  
33 33 # Return if this is not a push to a branch (e.g. new commits)
... ... @@ -35,7 +35,7 @@ module GitPush
35 35  
36 36 data = post_receive_data(oldrev, newrev, ref, user)
37 37  
38   - web_hooks.each { |web_hook| web_hook.execute(data) }
  38 + hooks.each { |hook| hook.execute(data) }
39 39 end
40 40  
41 41 def post_receive_data(oldrev, newrev, ref, user)
... ... @@ -97,7 +97,7 @@ module GitPush
97 97 self.update_merge_requests(oldrev, newrev, ref, user)
98 98  
99 99 # Execute web hooks
100   - self.execute_web_hooks(oldrev, newrev, ref, user)
  100 + self.execute_hooks(oldrev, newrev, ref, user)
101 101  
102 102 # Create satellite
103 103 self.satellite.create unless self.satellite.exists?
... ...
app/views/admin/hooks/_data_ex.html.erb 0 → 100644
... ... @@ -0,0 +1,66 @@
  1 +<% data_ex_str = <<eos
  2 +1. Project created:
  3 +{
  4 + "created_at": "2012-07-21T07:30:54Z",
  5 + "event_name": "project_create",
  6 + "name": "StoreCloud",
  7 + "owner_email": "johnsmith@gmail.com",
  8 + "owner_name": "John Smith",
  9 + "path": "storecloud",
  10 + "project_id": 74
  11 +}
  12 +
  13 +2. Project destroyed:
  14 +{
  15 + "event_name": "project_destroy",
  16 + "name": "Underscore",
  17 + "owner_email": "johnsmith@gmail.com",
  18 + "owner_name": "John Smith",
  19 + "path": "underscore",
  20 + "project_id": 73
  21 +}
  22 +
  23 +3. New Team Member:
  24 +{
  25 + "created_at": "2012-07-21T07:30:56Z",
  26 + "event_name": "user_add_to_team",
  27 + "project_access": "Master",
  28 + "project_id": 74,
  29 + "project_name": "StoreCloud",
  30 + "project_path": "storecloud",
  31 + "owner_email": "johnsmith@gmail.com",
  32 + "owner_name": "John Smith",
  33 +}
  34 +
  35 +4. Team Member Removed:
  36 +{
  37 + "created_at": "2012-07-21T07:30:56Z",
  38 + "event_name": "user_remove_from_team",
  39 + "project_access": "Master",
  40 + "project_id": 74,
  41 + "project_name": "StoreCloud",
  42 + "project_path": "storecloud",
  43 + "owner_email": "johnsmith@gmail.com",
  44 + "owner_name": "John Smith",
  45 +}
  46 +
  47 +5. User created:
  48 +{
  49 + "created_at": "2012-07-21T07:44:07Z",
  50 + "email": "js@gitlabhq.com",
  51 + "event_name": "user_create",
  52 + "name": "John Smith"
  53 +}
  54 +
  55 +6. User removed:
  56 +{
  57 + "created_at": "2012-07-21T07:44:07Z",
  58 + "email": "js@gitlabhq.com",
  59 + "event_name": "user_destroy",
  60 + "name": "John Smith"
  61 +}
  62 +
  63 +eos
  64 +%>
  65 +<% js_lexer = Pygments::Lexer[:js] %>
  66 +<%= raw js_lexer.highlight(data_ex_str) %>
... ...
app/views/admin/hooks/index.html.haml 0 → 100644
... ... @@ -0,0 +1,39 @@
  1 +.alert.alert-info
  2 + %span
  3 + Post receive hooks for binding events.
  4 + %br
  5 + Read more about system hooks
  6 + %strong #{link_to "here", help_system_hooks_path, :class => "vlink"}
  7 +
  8 += form_for @hook, :as => :hook, :url => admin_hooks_path do |f|
  9 + -if @hook.errors.any?
  10 + .alert-message.block-message.error
  11 + - @hook.errors.full_messages.each do |msg|
  12 + %p= msg
  13 + .clearfix
  14 + = f.label :url, "URL:"
  15 + .input
  16 + = f.text_field :url, :class => "text_field xxlarge"
  17 + &nbsp;
  18 + = f.submit "Add System Hook", :class => "btn primary"
  19 +%hr
  20 +
  21 +-if @hooks.any?
  22 + %h3
  23 + Hooks
  24 + %small (#{@hooks.count})
  25 + %br
  26 + %table.admin-table
  27 + %tr
  28 + %th URL
  29 + %th Method
  30 + %th
  31 + - @hooks.each do |hook|
  32 + %tr
  33 + %td
  34 + = link_to admin_hook_path(hook) do
  35 + %strong= hook.url
  36 + = link_to 'Test Hook', admin_hook_test_path(hook), :class => "btn small right"
  37 + %td POST
  38 + %td
  39 + = link_to 'Remove', admin_hook_path(hook), :confirm => 'Are you sure?', :method => :delete, :class => "danger btn small right"
... ...
app/views/admin/logs/show.html.haml
1   -%h4
2   - %i.icon-file
3   - githost.log
4   -%pre.logs
5   - - Gitlab::Logger.read_latest.each do |line|
6   - %span.log= line
  1 +.file_holder#README
  2 + .file_title
  3 + %i.icon-file
  4 + githost.log
  5 + .file_content.logs
  6 + %ol
  7 + - Gitlab::Logger.read_latest.each do |line|
  8 + %li
  9 + %p= line
... ...
app/views/admin/mailer/preview.html.haml
... ... @@ -1,28 +0,0 @@
1   -%p This is page with preview for all system emails that are sent to user
2   -%p Email previews built based on existing Project/Commit/Issue base - so some preview maybe unavailable unless object appear in system
3   -
4   -#accordion
5   - %h3
6   - %a New user
7   - %div
8   - %iframe{ :src=> admin_mailer_preview_user_new_path, :width=>"100%", :height=>"350"}
9   - %h3
10   - %a New issue
11   - %div
12   - %iframe{ :src=> admin_mailer_preview_issue_new_path, :width=>"100%", :height=>"350"}
13   - %h3
14   - %a Commit note
15   - %div
16   - %iframe{ :src=> admin_mailer_preview_note_path(:type => "Commit"), :width=>"100%", :height=>"350"}
17   - %h3
18   - %a Issue note
19   - %div
20   - %iframe{ :src=> admin_mailer_preview_note_path(:type => "Issue"), :width=>"100%", :height=>"350"}
21   - %h3
22   - %a Wall note
23   - %div
24   - %iframe{ :src=> admin_mailer_preview_note_path(:type => "Wall"), :width=>"100%", :height=>"350"}
25   -
26   -:javascript
27   - $(function() {
28   - $("#accordion").accordion(); });
app/views/admin/projects/index.html.haml
... ... @@ -13,8 +13,8 @@
13 13 %th Team Members
14 14 %th Post Receive
15 15 %th Last Commit
16   - %th
17   - %th
  16 + %th Edit
  17 + %th.cred Danger Zone!
18 18  
19 19 - @admin_projects.each do |project|
20 20 %tr
... ... @@ -24,5 +24,5 @@
24 24 %td= check_box_tag :post_receive_file, 1, project.has_post_receive_file?, :disabled => true
25 25 %td= last_commit(project)
26 26 %td= link_to 'Edit', edit_admin_project_path(project), :id => "edit_#{dom_id(project)}", :class => "btn small"
27   - %td= link_to 'Destroy', [:admin, project], :confirm => 'Are you sure?', :method => :delete, :class => "btn small danger"
  27 + %td.bgred= link_to 'Destroy', [:admin, project], :confirm => "REMOVE #{project.name}? Are you sure?", :method => :delete, :class => "btn small danger"
28 28 = paginate @admin_projects, :theme => "admin"
... ...
app/views/admin/users/_form.html.haml
... ... @@ -50,7 +50,7 @@
50 50  
51 51 .alert
52 52 .clearfix
53   - %p Give user ability to manage application.
  53 + %p Make the user a GitLab administrator.
54 54 = f.label :admin, :class => "checkbox" do
55 55 = f.check_box :admin
56 56 %span Administrator
... ... @@ -59,11 +59,11 @@
59 59 - if @admin_user.blocked
60 60 %span
61 61 = link_to 'Unblock', unblock_admin_user_path(@admin_user), :method => :put, :class => "btn small"
62   - This user is blocked and is not able to login GitLab
  62 + This user is blocked and is not able to login to GitLab
63 63 - else
64 64 %span
65 65 = link_to 'Block', block_admin_user_path(@admin_user), :confirm => 'USER WILL BE BLOCKED! Are you sure?', :method => :put, :class => "btn small danger"
66   - Blocked user will removed from all projects &amp; will not be able to login to GitLab.
  66 + Blocked users will be removed from all projects &amp; will not be able to login to GitLab.
67 67 .actions
68 68 = f.submit 'Save', :class => "btn primary"
69 69 - if @admin_user.new_record?
... ...
app/views/admin/users/index.html.haml
... ... @@ -27,7 +27,7 @@
27 27 %th Projects
28 28 %th Edit
29 29 %th Blocked
30   - %th
  30 + %th.cred Danger Zone!
31 31  
32 32 - @admin_users.each do |user|
33 33 %tr
... ... @@ -41,6 +41,6 @@
41 41 = link_to 'Unblock', unblock_admin_user_path(user), :method => :put, :class => "btn small success"
42 42 - else
43 43 = link_to 'Block', block_admin_user_path(user), :confirm => 'USER WILL BE BLOCKED! Are you sure?', :method => :put, :class => "btn small danger"
44   - %td= link_to 'Destroy', [:admin, user], :confirm => 'USER WILL BE REMOVED! Are you sure?', :method => :delete, :class => "btn small danger"
  44 + %td.bgred= link_to 'Destroy', [:admin, user], :confirm => "USER #{user.name} WILL BE REMOVED! Are you sure?", :method => :delete, :class => "btn small danger"
45 45  
46 46 = paginate @admin_users, :theme => "admin"
... ...
app/views/commits/_commits.html.haml
1 1 - @commits.group_by { |c| c.committed_date.to_date }.each do |day, commits|
2 2 %div.ui-box
3   - %h5= day.stamp("28 Aug, 2010")
  3 + %h5.small
  4 + %i.icon-calendar
  5 + = day.stamp("28 Aug, 2010")
4 6 %ul.unstyled= render commits
... ...
app/views/commits/_diffs.html.haml
... ... @@ -35,7 +35,13 @@
35 35 - if file.text?
36 36 = render "commits/text_file", :diff => diff, :index => i
37 37 - elsif file.image?
38   - .diff_file_content_image{:class => image_diff_class(diff)}
39   - %img{:src => "data:#{file.mime_type};base64,#{Base64.encode64(file.data)}"}
  38 + - if diff.renamed_file || diff.new_file || diff.deleted_file
  39 + .diff_file_content_image
  40 + %img{:class => image_diff_class(diff), :src => "data:#{file.mime_type};base64,#{Base64.encode64(file.data)}"}
  41 + - else
  42 + - old_file = (@commit.prev_commit.tree / diff.old_path)
  43 + .diff_file_content_image.img_compared
  44 + %img{:class => "diff_image_removed", :src => "data:#{file.mime_type};base64,#{Base64.encode64(old_file.data)}"}
  45 + %img{:class => "diff_image_added", :src => "data:#{file.mime_type};base64,#{Base64.encode64(file.data)}"}
40 46 - else
41 47 %p.nothing_here_message No preview for this file type
... ...
app/views/commits/_head.html.haml
... ... @@ -13,12 +13,12 @@
13 13 %li{:class => "#{branches_tab_class}"}
14 14 = link_to project_repository_path(@project) do
15 15 Branches
16   - %span.number= @project.repo.branch_count
  16 + %span.badge= @project.repo.branch_count
17 17  
18 18 %li{:class => "#{'active' if current_page?(tags_project_repository_path(@project)) }"}
19 19 = link_to tags_project_repository_path(@project) do
20 20 Tags
21   - %span.number= @project.repo.tag_count
  21 + %span.badge= @project.repo.tag_count
22 22  
23 23  
24 24 - if current_page?(project_commits_path(@project)) && current_user.private_token
... ...
app/views/commits/compare.html.haml
... ... @@ -20,7 +20,7 @@
20 20 = "..."
21 21 = text_field_tag :to, params[:to], :placeholder => "aa8b4ef", :class => "xlarge"
22 22 .actions
23   - = submit_tag "Compare", :class => "btn primary"
  23 + = submit_tag "Compare", :class => "btn btn-primary"
24 24  
25 25  
26 26 - unless @commits.empty?
... ...
app/views/dashboard/index.atom.builder
... ... @@ -8,17 +8,10 @@ xml.feed &quot;xmlns&quot; =&gt; &quot;http://www.w3.org/2005/Atom&quot;, &quot;xmlns:media&quot; =&gt; &quot;http://sear
8 8  
9 9 @events.each do |event|
10 10 if event.allowed?
  11 + event = EventDecorator.decorate(event)
11 12 xml.entry do
12   - if event.issue?
13   - event_link = project_issue_url(event.project, event.issue)
14   - event_title = event.issue_title
15   - elsif event.merge_request?
16   - event_link = project_merge_request_url(event.project, event.merge_request)
17   - event_title = event.merge_request_title
18   - elsif event.push?
19   - event_link = project_commits_url(event.project, :ref => event.ref_name)
20   - event_title = event.ref_name
21   - end
  13 + event_link = event.feed_url
  14 + event_title = event.feed_title
22 15  
23 16 xml.id "tag:#{request.host},#{event.created_at.strftime("%Y-%m-%d")}:#{event.id}"
24 17 xml.link :href => event_link
... ...
app/views/dashboard/index.html.haml
... ... @@ -10,9 +10,10 @@
10 10 add new key
11 11 to your profile
12 12 - if @events.any?
13   - = render @events
  13 + .content_list= render @events
14 14 - else
15 15 %h4.nothing_here_message Projects activity will be displayed here
  16 + .loading.hide
16 17 .side
17 18 = render "events/event_last_push", :event => @last_push
18 19 .projects_box
... ... @@ -54,3 +55,7 @@
54 55 New Project »
55 56 - else
56 57 If you will be added to project - it will be displayed here
  58 +
  59 +
  60 +:javascript
  61 + $(function(){ Pager.init(20); });
... ...
app/views/dashboard/index.js.haml
1 1 :plain
2   - $(".projects .activities").append("#{escape_javascript(render(@events))}");
  2 + Pager.append(#{@events.count}, "#{escape_javascript(render(@events))}");
... ...
app/views/events/_event_issue.html.haml
1 1 = image_tag gravatar_icon(event.author_email), :class => "avatar"
2 2 %strong #{event.author_name}
3   -%span.event_label= event.action_name
4   -&nbsp;issue
  3 +%span.event_label{:class => event.action_name}= event.action_name
  4 +issue
5 5 = link_to project_issue_path(event.project, event.issue) do
6 6 %strong= truncate event.issue_title
7 7 at
... ...
app/views/events/_event_last_push.html.haml
... ... @@ -5,12 +5,9 @@
5 5 %span Your pushed to
6 6 = event.ref_type
7 7 = link_to project_commits_path(event.project, :ref => event.ref_name) do
8   - %strong= event.ref_name
  8 + %strong= truncate(event.ref_name, :length => 28)
9 9 at
10 10 %strong= link_to event.project.name, event.project
11   - %span.cgray
12   - = time_ago_in_words(event.created_at)
13   - ago.
14 11  
15 12 = link_to new_mr_path_from_push_event(event), :title => "New Merge Request", :class => "btn very_small primary" do
16 13 Create Merge Request
... ...
app/views/events/_event_merge_request.html.haml
... ... @@ -2,8 +2,8 @@
2 2 .event_icon= image_tag "event_mr_merged.png"
3 3 = image_tag gravatar_icon(event.author_email), :class => "avatar"
4 4 %strong #{event.author_name}
5   -%span.event_label= event.action_name
6   -&nbsp;merge request
  5 +%span.event_label{:class => event.action_name}= event.action_name
  6 +merge request
7 7 = link_to project_merge_request_path(event.project, event.merge_request) do
8 8 %strong= truncate event.merge_request_title
9 9 at
... ...
app/views/events/_event_push.html.haml
... ... @@ -2,7 +2,7 @@
2 2 .event_icon= image_tag "event_push.png"
3 3 = image_tag gravatar_icon(event.author_email), :class => "avatar"
4 4 %strong #{event.author_name}
5   - %span.event_label= event.push_action_name
  5 + %span.event_label.pushed= event.push_action_name
6 6 = event.ref_type
7 7 = link_to project_commits_path(event.project, :ref => event.ref_name) do
8 8 %strong= event.ref_name
... ...
app/views/help/api.html.haml 0 → 100644
... ... @@ -0,0 +1,41 @@
  1 +%h3 API
  2 +.back_link
  3 + = link_to help_path do
  4 + &larr; to index
  5 +%hr
  6 +
  7 +%ol
  8 + %li
  9 + %a{:href => "#README"} README
  10 + %li
  11 + %a{:href => "#projects"} Projects
  12 + %li
  13 + %a{:href => "#users"} Users
  14 +
  15 +.file_holder#README
  16 + .file_title
  17 + %i.icon-file
  18 + README
  19 + .file_content.wiki
  20 + = preserve do
  21 + = markdown File.read(Rails.root.join("doc", "api", "README.md"))
  22 +
  23 +%br
  24 +
  25 +.file_holder#projects
  26 + .file_title
  27 + %i.icon-file
  28 + Projects
  29 + .file_content.wiki
  30 + = preserve do
  31 + = markdown File.read(Rails.root.join("doc", "api", "projects.md"))
  32 +
  33 +%br
  34 +
  35 +.file_holder#users
  36 + .file_title
  37 + %i.icon-file
  38 + Users
  39 + .file_content.wiki
  40 + = preserve do
  41 + = markdown File.read(Rails.root.join("doc", "api", "users.md"))
... ...
app/views/help/index.html.haml
... ... @@ -22,3 +22,9 @@
22 22  
23 23 %li
24 24 %h5= link_to "Web Hooks", help_web_hooks_path
  25 +
  26 + %li
  27 + %h5= link_to "System Hooks", help_system_hooks_path
  28 +
  29 + %li
  30 + %h5= link_to "API", help_api_path
... ...
app/views/help/system_hooks.html.haml 0 → 100644
... ... @@ -0,0 +1,13 @@
  1 +%h3 System hooks
  2 +.back_link
  3 + = link_to :back do
  4 + &larr; back
  5 +%hr
  6 +
  7 +%p.slead
  8 + Your Gitlab instance can perform HTTP POST request on next event: create_project, delete_project, create_user, delete_user, change_team_member.
  9 + %br
  10 + System Hooks can be used for logging or change information in LDAP server.
  11 + %br
  12 +%h5 Hooks request example:
  13 += render "admin/hooks/data_ex"
... ...
app/views/issues/_issues.html.haml
... ... @@ -6,7 +6,9 @@
6 6 .row
7 7 .span7= paginate @issues, :remote => true, :theme => "gitlab"
8 8 .span3.right
9   - %span.cgray.right #{@issues.total_count} issues for this filter
  9 + %span.cgray.right
  10 + %span.issue_counter #{@issues.total_count}
  11 + issues for this filter
10 12 - else
11 13 %li
12 14 %h4.nothing_here_message Nothing to show here
... ...
app/views/issues/_show.html.haml
... ... @@ -12,9 +12,9 @@
12 12 = issue.notes.count
13 13 - if can? current_user, :modify_issue, issue
14 14 - if issue.closed
15   - = link_to 'Reopen', project_issue_path(issue.project, issue, :issue => {:closed => false }, :status_only => true), :method => :put, :class => "btn small grouped", :remote => true
  15 + = link_to 'Reopen', project_issue_path(issue.project, issue, :issue => {:closed => false }, :status_only => true), :method => :put, :class => "btn small grouped reopen_issue", :remote => true
16 16 - else
17   - = link_to 'Resolve', project_issue_path(issue.project, issue, :issue => {:closed => true }, :status_only => true), :method => :put, :class => "success btn small grouped", :remote => true
  17 + = link_to 'Resolve', project_issue_path(issue.project, issue, :issue => {:closed => true }, :status_only => true), :method => :put, :class => "success btn small grouped close_issue", :remote => true
18 18 = link_to edit_project_issue_path(issue.project, issue), :class => "btn small edit-issue-link", :remote => true do
19 19 %i.icon-edit
20 20 Edit
... ... @@ -35,6 +35,4 @@
35 35 &nbsp;
36 36  
37 37 - if issue.upvotes > 0
38   - %span.badge.badge-success= "+#{issue.upvotes}"
39   -
40   -
  38 + %span.badge.badge-success= "+#{issue.upvotes}"
41 39 \ No newline at end of file
... ...
app/views/issues/index.html.haml
... ... @@ -2,7 +2,7 @@
2 2 .issues_content
3 3 %h3.page_title
4 4 Issues
5   - %small (#{@issues.total_count})
  5 + %small (<span class=issue_counter>#{@issues.total_count}</span>)
6 6 .right
7 7 .span5
8 8 - if can? current_user, :write_issue, @project
... ... @@ -45,4 +45,4 @@
45 45 :javascript
46 46 $(function(){
47 47 issuesPage();
48 48 - })
  49 + })
49 50 \ No newline at end of file
... ...
app/views/keys/new.html.haml
1   -%h3 New key
  1 +%h3.page_title New key
2 2 %hr
3 3 = render 'form'
4 4  
... ... @@ -11,4 +11,4 @@
11 11 if( key_mail && key_mail.length > 0 && title.val() == '' ){
12 12 $('#key_title').val( key_mail );
13 13 }
14   - });
15 14 \ No newline at end of file
  15 + });
... ...
app/views/layouts/_project_menu.html.haml
... ... @@ -17,14 +17,14 @@
17 17 %li{:class => tab_class(:issues)}
18 18 = link_to project_issues_filter_path(@project) do
19 19 Issues
20   - %span.count= @project.issues.opened.count
  20 + %span.count.issue_counter= @project.issues.opened.count
21 21  
22 22 - if @project.repo_exists?
23 23 - if @project.merge_requests_enabled
24 24 %li{:class => tab_class(:merge_requests)}
25 25 = link_to project_merge_requests_path(@project) do
26 26 Merge Requests
27   - %span.count= @project.merge_requests.opened.count
  27 + %span.count.merge_counter= @project.merge_requests.opened.count
28 28  
29 29 - if @project.wall_enabled
30 30 %li{:class => tab_class(:wall)}
... ...
app/views/layouts/admin.html.haml
... ... @@ -15,7 +15,7 @@
15 15 %li{:class => tab_class(:admin_logs)}
16 16 = link_to "Logs", admin_logs_path
17 17 %li{:class => tab_class(:admin_emails)}
18   - = link_to "Emails", admin_emails_path
  18 + = link_to "Hooks", admin_hooks_path
19 19 %li{:class => tab_class(:admin_resque)}
20 20 = link_to "Resque", admin_resque_path
21 21  
... ...
app/views/layouts/devise.html.haml
... ... @@ -1,6 +0,0 @@
1   -!!! 5
2   -%html{ :lang => "en"}
3   - = render "layouts/head"
4   - %body.ui_basic.login-page
5   - = render :partial => "layouts/flash"
6   - .container= yield
app/views/layouts/devise_layout.html.haml 0 → 100644
... ... @@ -0,0 +1,6 @@
  1 +!!! 5
  2 +%html{ :lang => "en"}
  3 + = render "layouts/head"
  4 + %body.ui_basic.login-page
  5 + = render :partial => "layouts/flash"
  6 + .container= yield
... ...
app/views/layouts/profile.html.haml
... ... @@ -12,16 +12,17 @@
12 12 %li{:class => tab_class(:password)}
13 13 = link_to "Password", profile_password_path
14 14  
  15 + %li{:class => tab_class(:ssh_keys)}
  16 + = link_to keys_path do
  17 + SSH Keys
  18 + %span.count= current_user.keys.count
  19 +
15 20 %li{:class => tab_class(:token)}
16 21 = link_to "Token", profile_token_path
17 22  
18 23 %li{:class => tab_class(:design)}
19 24 = link_to "Design", profile_design_path
20 25  
21   - %li{:class => tab_class(:ssh_keys)}
22   - = link_to keys_path do
23   - SSH Keys
24   - %span.count= current_user.keys.count
25 26  
26 27 .content
27 28 = yield
... ...
app/views/merge_requests/_form.html.haml
... ... @@ -5,7 +5,8 @@
5 5 - @merge_request.errors.full_messages.each do |msg|
6 6 %li= msg
7 7  
8   - %h3.padded.cgray 1. Select Branches
  8 + %h4.cdark 1. Select Branches
  9 + %br
9 10  
10 11 .row
11 12 .span6
... ... @@ -30,14 +31,21 @@
30 31 .bottom_commit
31 32 .mr_target_commit
32 33  
33   - %h3.padded.cgray 2. Fill info
  34 + %h4.cdark 2. Fill info
  35 +
34 36 .clearfix
35   - = f.label :assignee_id, "Assign to", :class => "control-label"
36   - .controls= f.select(:assignee_id, @project.users.all.collect {|p| [ p.name, p.id ] }, { :include_blank => "Select user" }, :style => "width:250px")
  37 + .main_box
  38 + .top_box_content
  39 + = f.label :title do
  40 + %strong= "Title *"
  41 + .input= f.text_field :title, :class => "input-xxlarge pad", :maxlength => 255, :rows => 5
  42 + .middle_box_content
  43 + = f.label :assignee_id do
  44 + %i.icon-user
  45 + Assign to
  46 + .input= f.select(:assignee_id, @project.users.all.collect {|p| [ p.name, p.id ] }, { :include_blank => "Select user" }, :style => "width:250px")
37 47  
38 48 .control-group
39   - = f.label :title, :class => "control-label"
40   - .controls= f.text_field :title, :class => "input-xxlarge pad", :maxlength => 255, :rows => 5
41 49  
42 50 .form-actions
43 51 = f.submit 'Save', :class => "btn-primary btn"
... ...
app/views/merge_requests/_merge_request.html.haml
... ... @@ -15,12 +15,14 @@
15 15 &rarr;
16 16 = merge_request.target_branch
17 17 = image_tag gravatar_icon(merge_request.author_email), :class => "avatar"
  18 +
  19 + = link_to project_merge_request_path(merge_request.project, merge_request) do
  20 + %p.row_title= truncate(merge_request.title, :length => 80)
  21 +
18 22 %span.update-author
19   - %strong= merge_request.author_name
20   - authored
  23 + %small.cdark= "##{merge_request.id}"
  24 + authored by #{merge_request.author_name}
21 25 = time_ago_in_words(merge_request.created_at)
22 26 ago
23 27 - if merge_request.upvotes > 0
24 28 %span.badge.badge-success= "+#{merge_request.upvotes}"
25   - = link_to project_merge_request_path(merge_request.project, merge_request) do
26   - %p.row_title= truncate(merge_request.title, :length => 80)
... ...
app/views/merge_requests/edit.html.haml
1   -%h3
  1 +%h3.page_title
2 2 = "Edit merge request #{@merge_request.id}"
3 3 %hr
4 4 = render 'form'
... ...
app/views/merge_requests/new.html.haml
1   -%h3 New Merge Request
  1 +%h3.page_title New Merge Request
2 2 %hr
3 3 = render 'form'
... ...
app/views/merge_requests/show/_commits.html.haml
1 1 - if @commits.present?
2 2 .ui-box
3   - %h5 Commits (#{@commits.count})
  3 + %h5
  4 + %i.icon-list
  5 + Commits (#{@commits.count})
4 6 .merge-request-commits
5 7 - if @commits.count > 8
6 8 %ul.first_mr_commits.unstyled
... ...
app/views/merge_requests/show/_mr_box.html.haml
... ... @@ -13,9 +13,10 @@
13 13 = image_tag gravatar_icon(@merge_request.author_email), :width => 16, :class => "lil_av"
14 14 %strong.author= link_to_merge_request_author(@merge_request)
15 15  
16   - %cite.cgray and currently assigned to
17   - = image_tag gravatar_icon(@merge_request.assignee_email), :width => 16, :class => "lil_av"
18   - %strong.author= link_to_merge_request_assignee(@merge_request)
  16 + - if @merge_request.assignee
  17 + %cite.cgray and currently assigned to
  18 + = image_tag gravatar_icon(@merge_request.assignee_email), :width => 16, :class => "lil_av"
  19 + %strong.author= link_to_merge_request_assignee(@merge_request)
19 20  
20 21  
21 22 - if @merge_request.closed
... ...
app/views/notes/_form.html.haml
... ... @@ -32,4 +32,4 @@
32 32 %span Any file less than 10 MB
33 33  
34 34  
35   - = f.submit 'Add Comment', :class => "btn primary", :id => "submit_note"
  35 + = f.submit 'Add Comment', :class => "btn primary submit_note", :id => "submit_note"
... ...
app/views/notes/_per_line_form.html.haml
... ... @@ -24,7 +24,7 @@
24 24 = check_box_tag :notify_author, 1 , @note.noteable_type == "Commit"
25 25 %span Commit author
26 26 .actions
27   - = f.submit 'Add note', :class => "btn primary", :id => "submit_note"
  27 + = f.submit 'Add note', :class => "btn primary submit_note", :id => "submit_note"
28 28 = link_to "Close", "#", :class => "btn hide-button"
29 29  
30 30 :javascript
... ...
app/views/notes/_reply_button.html.haml
1 1 %tr.line_notes_row.reply
2 2 %td{:colspan => 3}
  3 + %i.icon-comment
3 4 = link_to "Reply", "#", :class => "line_note_reply_link", "line_code" => line_code, :title => "Add note for this line"
... ...
app/views/refs/_tree.html.haml
... ... @@ -13,7 +13,7 @@
13 13 = render :partial => "refs/tree_file", :locals => { :name => tree.name, :content => tree.data, :file => tree }
14 14 - else
15 15 - contents = tree.contents
16   - %table#tree-slider.bordered-table.table
  16 + %table#tree-slider.bordered-table.table{:class => "table_#{@hex_path}" }
17 17 %thead
18 18 %th Name
19 19 %th Last Update
... ... @@ -29,34 +29,39 @@
29 29 %td
30 30 %td
31 31  
  32 + - index = 0
32 33 - contents.select{ |i| i.is_a?(Grit::Tree)}.each do |content|
33   - = render :partial => "refs/tree_item", :locals => { :content => content }
  34 + = render :partial => "refs/tree_item", :locals => { :content => content, :index => (index += 1) }
34 35 - contents.select{ |i| i.is_a?(Grit::Blob)}.each do |content|
35   - = render :partial => "refs/tree_item", :locals => { :content => content }
  36 + = render :partial => "refs/tree_item", :locals => { :content => content, :index => (index += 1) }
36 37 - contents.select{ |i| i.is_a?(Grit::Submodule)}.each do |content|
37   - = render :partial => "refs/submodule_item", :locals => { :content => content }
  38 + = render :partial => "refs/submodule_item", :locals => { :content => content, :index => (index += 1) }
38 39  
39 40 - if content = contents.select{ |c| c.is_a?(Grit::Blob) and c.name =~ /^readme/i }.first
40   - #tree-readme-holder
41   - %h3= content.name
42   - .readme
  41 + .file_holder#README
  42 + .file_title
  43 + %i.icon-file
  44 + = content.name
  45 + .file_content.wiki
43 46 - if content.name =~ /\.(md|markdown)$/i
44 47 = preserve do
45 48 = markdown(content.data)
46 49 - else
47 50 = simple_format(content.data)
48 51  
49   -- if params[:path]
50   - - history_path = tree_file_project_ref_path(@project, @ref, params[:path])
51   -- else
52   - - history_path = tree_project_ref_path(@project, @ref)
53 52 :javascript
54 53 $(function(){
55 54 $('select#branch').selectmenu({style:'popup', width:200});
56 55 $('select#tag').selectmenu({style:'popup', width:200});
57 56 $('.project-refs-select').chosen();
58 57  
59   - history.pushState({ path: this.path }, '', "#{history_path}")
  58 + history.pushState({ path: this.path }, '', "#{@history_path}");
  59 +
  60 + });
  61 +
  62 + // Load last commit log for each file in tree
  63 + $(window).load(function(){
  64 + ajaxGet('#{@logs_path}');
60 65 });
61 66  
62 67  
... ...
app/views/refs/_tree_commit.html.haml 0 → 100644
... ... @@ -0,0 +1,3 @@
  1 +- if tm
  2 + %strong= link_to "[#{tm.user_name}]", project_team_member_path(@project, tm)
  3 += link_to truncate(content_commit.safe_message, :length => tm ? 30 : 50), project_commit_path(@project, content_commit.id), :class => "tree-commit-link"
... ...
app/views/refs/_tree_file.html.haml
1   -.view_file
2   - .view_file_header
  1 +.file_holder
  2 + .file_title
3 3 %i.icon-file
4 4 %span.file_name
5 5 = name
... ... @@ -10,26 +10,28 @@
10 10 = link_to "blame", blame_file_project_ref_path(@project, @ref, :path => params[:path]), :class => "btn very_small"
11 11 - if file.text?
12 12 - if name =~ /\.(md|markdown)$/i
13   - #tree-readme-holder
14   - .readme
15   - = preserve do
16   - = markdown(file.data)
  13 + .file_content.wiki
  14 + = preserve do
  15 + = markdown(file.data)
17 16 - else
18   - .view_file_content
  17 + .file_content.code
19 18 - unless file.empty?
20 19 %div{:class => current_user.dark_scheme ? "black" : "white"}
21 20 = preserve do
22 21 = raw file.colorize(options: { linenos: 'True'})
23 22 - else
24 23 %h4.nothing_here_message Empty file
  24 +
25 25 - elsif file.image?
26   - .view_file_content_image
  26 + .file_content.image_file
27 27 %img{ :src => "data:#{file.mime_type};base64,#{Base64.encode64(file.data)}"}
  28 +
28 29 - else
29   - %center
30   - = link_to blob_project_ref_path(@project, @ref, :path => params[:path]) do
31   - %div.padded
32   - %br
33   - = image_tag "download.png", :width => 64
34   - %h3
35   - Download (#{file.mb_size})
  30 + .file_content.blob_file
  31 + %center
  32 + = link_to blob_project_ref_path(@project, @ref, :path => params[:path]) do
  33 + %div.padded
  34 + %br
  35 + = image_tag "download.png", :width => 64
  36 + %h3
  37 + Download (#{file.mb_size})
... ...
app/views/refs/_tree_item.html.haml
1   -- file = params[:path] ? File.join(params[:path], content.name) : content.name
2   -- content_commit = @project.commits(@commit.id, file, 1).last
3   -- return unless content_commit
4   -%tr{ :class => "tree-item", :url => tree_file_project_ref_path(@project, @ref, file) }
  1 +- file = tree_full_path(content)
  2 +%tr{ :class => "tree-item #{tree_hex_class(content)}", :url => tree_file_project_ref_path(@project, @ref, file) }
5 3 %td.tree-item-file-name
6   - - if content.is_a?(Grit::Blob)
7   - - if content.text?
8   - = image_tag "file_txt.png"
9   - - elsif content.image?
10   - = image_tag "file_img.png"
11   - - else
12   - = image_tag "file_bin.png"
13   - - else
14   - = image_tag "file_dir.png"
  4 + = tree_icon(content)
15 5 = link_to truncate(content.name, :length => 40), tree_file_project_ref_path(@project, @ref || @commit.id, file), :remote => :true
16   - %td.cgray
17   - = time_ago_in_words(content_commit.committed_date)
18   - ago
19   - %td.commit
20   - - tm = @project.team_member_by_name_or_email(content_commit.author_email, content_commit.author_name)
21   - - if tm
22   - %strong= link_to "[#{tm.user_name}]", project_team_member_path(@project, tm)
23   - = link_to truncate(content_commit.safe_message, :length => tm ? 30 : 50), project_commit_path(@project, content_commit.id), :class => "tree-commit-link"
  6 + %td.tree_time_ago.cgray
  7 + - if index == 1
  8 + %span.log_loading
  9 + Loading commit data..
  10 + = image_tag "ajax_loader_tree.gif", :width => 14
  11 + %td.tree_commit
... ...
app/views/refs/blame.html.haml
... ... @@ -11,8 +11,8 @@
11 11 %li= link
12 12 .clear
13 13  
14   - .view_file.blame_file
15   - .view_file_header
  14 + .file_holder
  15 + .file_title
16 16 %i.icon-file
17 17 %span.file_name
18 18 = @tree.name
... ... @@ -21,7 +21,7 @@
21 21 = link_to "raw", blob_project_ref_path(@project, @ref, :path => params[:path]), :class => "btn very_small", :target => "_blank"
22 22 = link_to "history", project_commits_path(@project, :path => params[:path], :ref => @ref), :class => "btn very_small"
23 23 = link_to "source", tree_file_project_ref_path(@project, @ref, :path => params[:path]), :class => "btn very_small"
24   - .view_file_content
  24 + .file_content.blame
25 25 %table
26 26 - @blame.each do |commit, lines|
27 27 - commit = Commit.new(commit)
... ... @@ -29,7 +29,7 @@
29 29 %td.author
30 30 = image_tag gravatar_icon(commit.author_email, 16)
31 31 = commit.author_name
32   - %td.commit
  32 + %td.blame_commit
33 33 &nbsp;
34 34 = link_to project_commit_path(@project, :id => commit.id) do
35 35 %code= commit.id.to_s[0..10]
... ... @@ -37,8 +37,7 @@
37 37 %td.lines
38 38 = preserve do
39 39 %pre
40   - - lines.each do |line|
41   - = line
  40 + = Gitlab::Encode.utf8 lines.join("\n")
42 41  
43 42 :javascript
44 43 $(function(){
... ...
app/views/refs/logs_tree.js.haml 0 → 100644
... ... @@ -0,0 +1,9 @@
  1 +- @logs.each do |content_data|
  2 + - file_name = content_data[:file_name]
  3 + - content_commit = content_data[:commit]
  4 + - tm = @project.team_member_by_name_or_email(content_commit.author_email, content_commit.author_name)
  5 +
  6 + :plain
  7 + var row = $("table.table_#{@hex_path} tr.file_#{hexdigest(file_name)}");
  8 + row.find("td.tree_time_ago").html('#{escape_javascript(time_ago_in_words(content_commit.committed_date))} ago');
  9 + row.find("td.tree_commit").html('#{escape_javascript(render("tree_commit", :tm => tm, :content_commit => content_commit))}');
... ...
app/views/refs/tree.js.haml
1 1 :plain
  2 + // Load Files list
2 3 $("#tree-holder").html("#{escape_javascript(render(:partial => "tree", :locals => {:repo => @repo, :commit => @commit, :tree => @tree}))}");
3 4 $("#tree-content-holder").show("slide", { direction: "right" }, 150);
4 5 $('.project-refs-form #path').val("#{params[:path]}");
  6 +
  7 + // Load last commit log for each file in tree
  8 + $('#tree-slider').waitForImages(function() {
  9 + ajaxGet('#{@logs_path}');
  10 + });
... ...
app/views/snippets/show.html.haml
... ... @@ -7,16 +7,14 @@
7 7 = link_to "Edit", edit_project_snippet_path(@project, @snippet), :class => "btn small right"
8 8  
9 9 %br
10   -#tree-holder
11   - #tree-content-holder
12   - .view_file
13   - .view_file_header
14   - %i.icon-file
15   - %strong= @snippet.file_name
16   - %span.options
17   - = link_to "raw", raw_project_snippet_path(@project, @snippet), :class => "btn very_small", :target => "_blank"
18   - .view_file_content
19   - %div{:class => current_user.dark_scheme ? "black" : ""}
20   - = raw @snippet.colorize(options: { linenos: 'True'})
  10 +.file_holder
  11 + .file_title
  12 + %i.icon-file
  13 + %strong= @snippet.file_name
  14 + %span.options
  15 + = link_to "raw", raw_project_snippet_path(@project, @snippet), :class => "btn very_small", :target => "_blank"
  16 + .file_content.code
  17 + %div{:class => current_user.dark_scheme ? "black" : ""}
  18 + = raw @snippet.colorize(options: { linenos: 'True'})
21 19  
22 20 = render "notes/notes", :tid => @snippet.id, :tt => "snippet"
... ...
app/workers/system_hook_worker.rb 0 → 100644
... ... @@ -0,0 +1,7 @@
  1 +class SystemHookWorker
  2 + @queue = :system_hook
  3 +
  4 + def self.perform(hook_id, data)
  5 + SystemHook.find(hook_id).execute data
  6 + end
  7 +end
... ...
config/application.rb
... ... @@ -23,7 +23,7 @@ module Gitlab
23 23 # config.plugins = [ :exception_notification, :ssl_requirement, :all ]
24 24  
25 25 # Activate observers that should always be running.
26   - config.active_record.observers = :mailer_observer, :activity_observer, :project_observer, :key_observer, :issue_observer, :user_observer
  26 + config.active_record.observers = :mailer_observer, :activity_observer, :project_observer, :key_observer, :issue_observer, :user_observer, :system_hook_observer
27 27  
28 28 # Set Time.zone default to the specified zone and make Active Record auto-convert to this zone.
29 29 # Run "rake -D time" for a list of tasks for finding time zone names. Default is UTC.
... ...
config/gitlab.yml.example
... ... @@ -21,6 +21,8 @@ email:
21 21 # Like default project limit for user etc
22 22 app:
23 23 default_projects_limit: 10
  24 + # backup_path: "/vol/backups" # default: Rails.root + backups/
  25 + # backup_keep_time: 604800 # default: 0 (forever) (in seconds)
24 26  
25 27  
26 28 #
... ...
config/initializers/1_settings.rb
... ... @@ -95,11 +95,21 @@ class Settings &lt; Settingslogic
95 95 end
96 96  
97 97 def gitolite_admin_uri
98   - git['admin_uri'] || 'git@localhost:gitolite-admin'
  98 + git_host['admin_uri'] || 'git@localhost:gitolite-admin'
99 99 end
100 100  
101 101 def default_projects_limit
102 102 app['default_projects_limit'] || 10
103 103 end
  104 +
  105 + def backup_path
  106 + t = app['backup_path'] || "backups/"
  107 + t = /^\//.match(t) ? t : File.join(Rails.root + t)
  108 + t
  109 + end
  110 +
  111 + def backup_keep_time
  112 + app['backup_keep_time'] || 0
  113 + end
104 114 end
105 115 end
... ...
config/initializers/devise.rb
... ... @@ -93,10 +93,6 @@ Devise.setup do |config|
93 93 # If true, extends the user's remember period when remembered via cookie.
94 94 # config.extend_remember_period = false
95 95  
96   - # If true, uses the password salt as remember token. This should be turned
97   - # to false if you are not using database authenticatable.
98   - config.use_salt_as_remember_token = true
99   -
100 96 # Options to be passed to the created cookie. For instance, you can set
101 97 # :secure => true in order to force SSL only cookies.
102 98 # config.cookie_options = {}
... ... @@ -119,7 +115,7 @@ Devise.setup do |config|
119 115 # Defines which strategy will be used to lock an account.
120 116 # :failed_attempts = Locks an account after a number of failed attempts to sign in.
121 117 # :none = No lock strategy. You should handle locking by yourself.
122   - # config.lock_strategy = :failed_attempts
  118 + config.lock_strategy = :failed_attempts
123 119  
124 120 # Defines which key will be used when locking and unlocking an account
125 121 # config.unlock_keys = [ :email ]
... ... @@ -129,14 +125,14 @@ Devise.setup do |config|
129 125 # :time = Re-enables login after a certain amount of time (see :unlock_in below)
130 126 # :both = Enables both strategies
131 127 # :none = No unlock strategy. You should handle unlocking by yourself.
132   - # config.unlock_strategy = :both
  128 + config.unlock_strategy = :time
133 129  
134 130 # Number of authentication tries before locking an account if lock_strategy
135 131 # is failed attempts.
136   - # config.maximum_attempts = 20
  132 + config.maximum_attempts = 10
137 133  
138 134 # Time interval to unlock the account if :time is enabled as unlock_strategy.
139   - # config.unlock_in = 1.hour
  135 + config.unlock_in = 10.minutes
140 136  
141 137 # ==> Configuration for :recoverable
142 138 #
... ... @@ -160,9 +156,9 @@ Devise.setup do |config|
160 156 # Defines name of the authentication token params key
161 157 config.token_authentication_key = :private_token
162 158  
163   - # If true, authentication through token does not store user in session and needs
  159 + # Authentication through token does not store user in session and needs
164 160 # to be supplied on each request. Useful if you are using the token as API token.
165   - config.stateless_token = true
  161 + config.skip_session_storage << :token_auth
166 162  
167 163 # ==> Scopes configuration
168 164 # Turn scoped views on. Before rendering "sessions/new", it will first check for
... ...
config/locales/devise.en.yml
... ... @@ -35,13 +35,11 @@ en:
35 35 confirmed: 'Your account was successfully confirmed. You are now signed in.'
36 36 registrations:
37 37 signed_up: 'Welcome! You have signed up successfully.'
38   - inactive_signed_up: 'You have signed up successfully. However, we could not sign you in because your account is %{reason}.'
39 38 updated: 'You updated your account successfully.'
40 39 destroyed: 'Bye! Your account was successfully cancelled. We hope to see you again soon.'
41   - reasons:
42   - inactive: 'inactive'
43   - unconfirmed: 'unconfirmed'
44   - locked: 'locked'
  40 + signed_up_but_unconfirmed: 'A message with a confirmation link has been sent to your email address. Please open the link to activate your account.'
  41 + signed_up_but_inactive: 'You have signed up successfully. However, we could not sign you in because your account is not yet activated.'
  42 + signed_up_but_locked: 'You have signed up successfully. However, we could not sign you in because your account is locked.'
45 43 unlocks:
46 44 send_instructions: 'You will receive an email with instructions about how to unlock your account in a few minutes.'
47 45 unlocked: 'Your account was successfully unlocked. You are now signed in.'
... ...
config/routes.rb
... ... @@ -26,7 +26,9 @@ Gitlab::Application.routes.draw do
26 26 get 'help' => 'help#index'
27 27 get 'help/permissions' => 'help#permissions'
28 28 get 'help/workflow' => 'help#workflow'
  29 + get 'help/api' => 'help#api'
29 30 get 'help/web_hooks' => 'help#web_hooks'
  31 + get 'help/system_hooks' => 'help#system_hooks'
30 32  
31 33 #
32 34 # Admin Area
... ... @@ -46,11 +48,13 @@ Gitlab::Application.routes.draw do
46 48 end
47 49 end
48 50 resources :team_members, :only => [:edit, :update, :destroy]
49   - get 'emails', :to => 'mailer#preview'
50 51 get 'mailer/preview_note'
51 52 get 'mailer/preview_user_new'
52 53 get 'mailer/preview_issue_new'
53 54  
  55 + resources :hooks, :only => [:index, :create, :destroy] do
  56 + get :test
  57 + end
54 58 resource :logs
55 59 resource :resque, :controller => 'resque'
56 60 root :to => "dashboard#index"
... ... @@ -116,6 +120,8 @@ Gitlab::Application.routes.draw do
116 120  
117 121 member do
118 122 get "tree", :constraints => { :id => /[a-zA-Z.\/0-9_\-]+/ }
  123 + get "logs_tree", :constraints => { :id => /[a-zA-Z.\/0-9_\-]+/ }
  124 +
119 125 get "blob",
120 126 :constraints => {
121 127 :id => /[a-zA-Z.0-9\/_\-]+/,
... ... @@ -131,6 +137,14 @@ Gitlab::Application.routes.draw do
131 137 :path => /.*/
132 138 }
133 139  
  140 + # tree viewer
  141 + get "logs_tree/:path" => "refs#logs_tree",
  142 + :as => :logs_file,
  143 + :constraints => {
  144 + :id => /[a-zA-Z.0-9\/_\-]+/,
  145 + :path => /.*/
  146 + }
  147 +
134 148 # blame
135 149 get "blame/:path" => "refs#blame",
136 150 :as => :blame_file,
... ...
db/migrate/20110913200833_devise_create_users.rb
1 1 class DeviseCreateUsers < ActiveRecord::Migration
2 2 def self.up
3 3 create_table(:users) do |t|
4   - t.database_authenticatable :null => false
5   - t.recoverable
6   - t.rememberable
7   - t.trackable
8   -
9   - # t.encryptable
10   - # t.confirmable
11   - # t.lockable :lock_strategy => :failed_attempts, :unlock_strategy => :both
12   - # t.token_authenticatable
  4 + ## Database authenticatable
  5 + t.string :email, :null => false, :default => ""
  6 + t.string :encrypted_password, :null => false, :default => ""
  7 +
  8 + ## Recoverable
  9 + t.string :reset_password_token
  10 + t.datetime :reset_password_sent_at
  11 +
  12 + ## Rememberable
  13 + t.datetime :remember_created_at
  14 +
  15 + ## Trackable
  16 + t.integer :sign_in_count, :default => 0
  17 + t.datetime :current_sign_in_at
  18 + t.datetime :last_sign_in_at
  19 + t.string :current_sign_in_ip
  20 + t.string :last_sign_in_ip
  21 +
  22 + ## Encryptable
  23 + # t.string :password_salt
  24 +
  25 + ## Confirmable
  26 + # t.string :confirmation_token
  27 + # t.datetime :confirmed_at
  28 + # t.datetime :confirmation_sent_at
  29 + # t.string :unconfirmed_email # Only if using reconfirmable
  30 +
  31 + ## Lockable
  32 + # t.integer :failed_attempts, :default => 0 # Only if lock strategy is :failed_attempts
  33 + # t.string :unlock_token # Only if unlock strategy is :email or :both
  34 + # t.datetime :locked_at
  35 +
  36 + # Token authenticatable
  37 + # t.string :authentication_token
  38 +
  39 + ## Invitable
  40 + # t.string :invitation_token
13 41  
14 42 t.timestamps
15 43 end
... ...
db/migrate/20120706065612_add_lockable_to_users.rb 0 → 100644
... ... @@ -0,0 +1,6 @@
  1 +class AddLockableToUsers < ActiveRecord::Migration
  2 + def change
  3 + add_column :users, :failed_attempts, :integer, :default => 0
  4 + add_column :users, :locked_at, :datetime
  5 + end
  6 +end
... ...
db/migrate/20120712080407_add_type_to_web_hook.rb 0 → 100644
... ... @@ -0,0 +1,5 @@
  1 +class AddTypeToWebHook < ActiveRecord::Migration
  2 + def change
  3 + add_column :web_hooks, :type, :string, :default => "ProjectHook"
  4 + end
  5 +end
... ...
db/schema.rb
... ... @@ -11,7 +11,7 @@
11 11 #
12 12 # It's strongly recommended to check this file into your version control system.
13 13  
14   -ActiveRecord::Schema.define(:version => 20120627145613) do
  14 +ActiveRecord::Schema.define(:version => 20120712080407) do
15 15  
16 16 create_table "events", :force => true do |t|
17 17 t.string "target_type"
... ... @@ -169,6 +169,8 @@ ActiveRecord::Schema.define(:version =&gt; 20120627145613) do
169 169 t.integer "theme_id", :default => 1, :null => false
170 170 t.string "bio"
171 171 t.boolean "blocked", :default => false, :null => false
  172 + t.integer "failed_attempts", :default => 0
  173 + t.datetime "locked_at"
172 174 end
173 175  
174 176 add_index "users", ["email"], :name => "index_users_on_email", :unique => true
... ... @@ -185,8 +187,9 @@ ActiveRecord::Schema.define(:version =&gt; 20120627145613) do
185 187 create_table "web_hooks", :force => true do |t|
186 188 t.string "url"
187 189 t.integer "project_id"
188   - t.datetime "created_at", :null => false
189   - t.datetime "updated_at", :null => false
  190 + t.datetime "created_at", :null => false
  191 + t.datetime "updated_at", :null => false
  192 + t.string "type", :default => "ProjectHook"
190 193 end
191 194  
192 195 create_table "wikis", :force => true do |t|
... ...
doc/installation.md
... ... @@ -60,7 +60,7 @@ Also read the [Read this before you submit an issue](https://github.com/gitlabhq
60 60 sudo apt-get update
61 61 sudo apt-get upgrade
62 62  
63   - sudo apt-get install -y wget curl gcc checkinstall libxml2-dev libxslt-dev sqlite3 libsqlite3-dev libcurl4-openssl-dev libreadline-gplv2-dev libc6-dev libssl-dev libmysql++-dev make build-essential zlib1g-dev libicu-dev redis-server openssh-server git-core python-dev python-pip libyaml-dev sendmail
  63 + sudo apt-get install -y wget curl gcc checkinstall libxml2-dev libxslt-dev sqlite3 libsqlite3-dev libcurl4-openssl-dev libreadline6-dev libc6-dev libssl-dev libmysql++-dev make build-essential zlib1g-dev libicu-dev redis-server openssh-server git-core python-dev python-pip libyaml-dev sendmail
64 64  
65 65 # If you want to use MySQL:
66 66 sudo apt-get install -y mysql-server mysql-client libmysqlclient-dev
... ... @@ -107,10 +107,10 @@ Get gitolite source code:
107 107  
108 108 Setup:
109 109  
110   - sudo -u git sh -c 'echo -e "PATH=\$PATH:/home/git/bin\nexport PATH" > /home/git/.profile'
  110 + sudo -u git sh -c 'echo -e "PATH=\$PATH:/home/git/bin\nexport PATH" >> /home/git/.profile'
111 111 sudo -u git -H sh -c "PATH=/home/git/bin:$PATH; /home/git/gitolite/src/gl-system-install"
112 112 sudo cp /home/gitlab/.ssh/id_rsa.pub /home/git/gitlab.pub
113   - sudo chmod 777 /home/git/gitlab.pub
  113 + sudo chmod 0444 /home/git/gitlab.pub
114 114  
115 115 sudo -u git -H sed -i 's/0077/0007/g' /home/git/share/gitolite/conf/example.gitolite.rc
116 116 sudo -u git -H sh -c "PATH=/home/git/bin:$PATH; gl-setup -q /home/git/gitlab.pub"
... ... @@ -139,6 +139,8 @@ Permissions:
139 139 cd /home/gitlab
140 140 sudo -H -u gitlab git clone -b stable git://github.com/gitlabhq/gitlabhq.git gitlab
141 141 cd gitlab
  142 +
  143 + sudo -u gitlab mkdir tmp
142 144  
143 145 # Rename config files
144 146 sudo -u gitlab cp config/gitlab.yml.example config/gitlab.yml
... ... @@ -216,15 +218,15 @@ Application can be started with next command:
216 218 sudo -u gitlab cp config/unicorn.rb.orig config/unicorn.rb
217 219 sudo -u gitlab bundle exec unicorn_rails -c config/unicorn.rb -E production -D
218 220  
219   -Edit /etc/nginx/nginx.conf. Add in **http** section:
  221 +Edit /etc/nginx/nginx.conf. In the *http* section add:
220 222  
221 223 upstream gitlab {
222 224 server unix:/home/gitlab/gitlab/tmp/sockets/gitlab.socket;
223 225 }
224 226  
225 227 server {
226   - listen YOUR_SERVER_IP:80;
227   - server_name gitlab.YOUR_DOMAIN.com;
  228 + listen YOUR_SERVER_IP:80; # e.g., listen 192.168.1.1:80;
  229 + server_name YOUR_SERVER_FQDN; # e.g., server_name source.example.com;
228 230 root /home/gitlab/gitlab/public;
229 231  
230 232 # individual nginx logs for this gitlab vhost
... ... @@ -232,26 +234,26 @@ Edit /etc/nginx/nginx.conf. Add in **http** section:
232 234 error_log /var/log/nginx/gitlab_error.log;
233 235  
234 236 location / {
235   - # serve static files from defined root folder;.
236   - # @gitlab is a named location for the upstream fallback, see below
237   - try_files $uri $uri/index.html $uri.html @gitlab;
  237 + # serve static files from defined root folder;.
  238 + # @gitlab is a named location for the upstream fallback, see below
  239 + try_files $uri $uri/index.html $uri.html @gitlab;
238 240 }
239 241  
240 242 # if a file, which is not found in the root folder is requested,
241 243 # then the proxy pass the request to the upsteam (gitlab unicorn)
242 244 location @gitlab {
243 245 proxy_redirect off;
  246 +
244 247 # you need to change this to "https", if you set "ssl" directive to "on"
245 248 proxy_set_header X-FORWARDED_PROTO http;
246   - proxy_set_header Host gitlab.YOUR_SUBDOMAIN.com:80;
  249 + proxy_set_header Host $http_host;
247 250 proxy_set_header X-Real-IP $remote_addr;
248 251  
249 252 proxy_pass http://gitlab;
250 253 }
251   -
252 254 }
253 255  
254   -gitlab.YOUR_DOMAIN.com - change to your domain.
  256 +Change **YOUR_SERVER_IP** and **YOUR_SERVER_FQDN** to the IP address and fully-qualified domain name of the host serving GitLab.
255 257  
256 258 Restart nginx:
257 259  
... ...
lib/gitlab/logger.rb
1 1 module Gitlab
2   - class Logger
  2 + class Logger < ::Logger
3 3 def self.error(message)
4   - @@logger ||= ::Logger.new(File.join(Rails.root, "log/githost.log"))
5   - message = Time.now.to_s(:long) + " -> " + message
6   - @@logger.error(message)
  4 + build.error(message)
  5 + end
  6 +
  7 + def self.info(message)
  8 + build.info(message)
7 9 end
8 10  
9 11 def self.read_latest
10 12 path = Rails.root.join("log/githost.log")
11   - logs = `tail -n 50 #{path}`.split("\n")
  13 + logs = File.read(path).split("\n")
12 14 end
  15 +
  16 + def self.build
  17 + new(File.join(Rails.root, "log/githost.log"))
  18 + end
  19 +
  20 + def format_message(severity, timestamp, progname, msg)
  21 + "#{timestamp.to_s(:long)} -> #{severity} -> #{msg}\n"
  22 + end
13 23 end
14 24 end
... ...
lib/tasks/gitlab/backup.rake 0 → 100644
... ... @@ -0,0 +1,190 @@
  1 +require 'active_record/fixtures'
  2 +
  3 +namespace :gitlab do
  4 + namespace :app do
  5 +
  6 + # Create backup of gitlab system
  7 + desc "GITLAB | Create a backup of the gitlab system"
  8 + task :backup_create => :environment do
  9 +
  10 + Rake::Task["gitlab:app:db_dump"].invoke
  11 + Rake::Task["gitlab:app:repo_dump"].invoke
  12 +
  13 + Dir.chdir(Gitlab.config.backup_path)
  14 +
  15 + # saving additional informations
  16 + s = Hash.new
  17 + s["db_version"] = "#{ActiveRecord::Migrator.current_version}"
  18 + s["backup_created_at"] = "#{Time.now}"
  19 + s["gitlab_version"] = %x{git rev-parse HEAD}.gsub(/\n/,"")
  20 + s["tar_version"] = %x{tar --version | head -1}.gsub(/\n/,"")
  21 +
  22 + File.open("#{Gitlab.config.backup_path}/backup_information.yml", "w+") do |file|
  23 + file << s.to_yaml.gsub(/^---\n/,'')
  24 + end
  25 +
  26 + # create archive
  27 + print "Creating backup archive: #{Time.now.to_i}_gitlab_backup.tar "
  28 + if Kernel.system("tar -cf #{Time.now.to_i}_gitlab_backup.tar repositories/ db/ backup_information.yml")
  29 + puts "[DONE]".green
  30 + else
  31 + puts "[FAILED]".red
  32 + end
  33 +
  34 + # cleanup: remove tmp files
  35 + print "Deletion of tmp directories..."
  36 + if Kernel.system("rm -rf repositories/ db/ backup_information.yml")
  37 + puts "[DONE]".green
  38 + else
  39 + puts "[FAILED]".red
  40 + end
  41 +
  42 + # delete backups
  43 + print "Deleting old backups... "
  44 + if Gitlab.config.backup_keep_time > 0
  45 + file_list = Dir.glob("*_gitlab_backup.tar").map { |f| f.split(/_/).first.to_i }
  46 + file_list.sort.each do |timestamp|
  47 + if Time.at(timestamp) < (Time.now - Gitlab.config.backup_keep_time)
  48 + %x{rm #{timestamp}_gitlab_backup.tar}
  49 + end
  50 + end
  51 + puts "[DONE]".green
  52 + else
  53 + puts "[SKIPPING]".yellow
  54 + end
  55 +
  56 + end
  57 +
  58 +
  59 + # Restore backup of gitlab system
  60 + desc "GITLAB | Restore a previously created backup"
  61 + task :backup_restore => :environment do
  62 +
  63 + Dir.chdir(Gitlab.config.backup_path)
  64 +
  65 + # check for existing backups in the backup dir
  66 + file_list = Dir.glob("*_gitlab_backup.tar").each.map { |f| f.split(/_/).first.to_i }
  67 + puts "no backup found" if file_list.count == 0
  68 + if file_list.count > 1 && ENV["BACKUP"].nil?
  69 + puts "Found more than one backup, please specify which one you want to restore:"
  70 + puts "rake gitlab:app:backup_restore BACKUP=timestamp_of_backup"
  71 + exit 1;
  72 + end
  73 +
  74 + tar_file = ENV["BACKUP"].nil? ? File.join(file_list.first.to_s + "_gitlab_backup.tar") : File.join(ENV["BACKUP"] + "_gitlab_backup.tar")
  75 +
  76 + unless File.exists?(tar_file)
  77 + puts "The specified backup doesn't exist!"
  78 + exit 1;
  79 + end
  80 +
  81 + print "Unpacking backup... "
  82 + unless Kernel.system("tar -xf #{tar_file}")
  83 + puts "[FAILED]".red
  84 + exit 1
  85 + else
  86 + puts "[DONE]".green
  87 + end
  88 +
  89 + settings = YAML.load_file("backup_information.yml")
  90 + ENV["VERSION"] = "#{settings["db_version"]}" if settings["db_version"].to_i > 0
  91 +
  92 + # restoring mismatching backups can lead to unexpected problems
  93 + if settings["gitlab_version"] != %x{git rev-parse HEAD}.gsub(/\n/,"")
  94 + puts "gitlab_version mismatch:".red
  95 + puts " Your current HEAD differs from the HEAD in the backup!".red
  96 + puts " Please switch to the following revision and try again:".red
  97 + puts " revision: #{settings["gitlab_version"]}".red
  98 + exit 1
  99 + end
  100 +
  101 + Rake::Task["gitlab:app:db_restore"].invoke
  102 + Rake::Task["gitlab:app:repo_restore"].invoke
  103 +
  104 + # cleanup: remove tmp files
  105 + print "Deletion of tmp directories..."
  106 + if Kernel.system("rm -rf repositories/ db/ backup_information.yml")
  107 + puts "[DONE]".green
  108 + else
  109 + puts "[FAILED]".red
  110 + end
  111 +
  112 + end
  113 +
  114 +
  115 + ################################################################################
  116 + ################################# invoked tasks ################################
  117 +
  118 + ################################# REPOSITORIES #################################
  119 +
  120 + task :repo_dump => :environment do
  121 + backup_path_repo = File.join(Gitlab.config.backup_path, "repositories")
  122 + FileUtils.mkdir_p(backup_path_repo) until Dir.exists?(backup_path_repo)
  123 + puts "Dumping repositories:"
  124 + project = Project.all.map { |n| [n.name,n.path_to_repo] }
  125 + project << ["gitolite-admin.git", File.join(File.dirname(project.first.second), "gitolite-admin.git")]
  126 + project.each do |project|
  127 + print "- Dumping repository #{project.first}... "
  128 + if Kernel.system("cd #{project.second} > /dev/null 2>&1 && git bundle create #{backup_path_repo}/#{project.first}.bundle --all > /dev/null 2>&1")
  129 + puts "[DONE]".green
  130 + else
  131 + puts "[FAILED]".red
  132 + end
  133 + end
  134 + end
  135 +
  136 + task :repo_restore => :environment do
  137 + backup_path_repo = File.join(Gitlab.config.backup_path, "repositories")
  138 + puts "Restoring repositories:"
  139 + project = Project.all.map { |n| [n.name,n.path_to_repo] }
  140 + project << ["gitolite-admin.git", File.join(File.dirname(project.first.second), "gitolite-admin.git")]
  141 + project.each do |project|
  142 + print "- Restoring repository #{project.first}... "
  143 + FileUtils.rm_rf(project.second) if File.dirname(project.second) # delet old stuff
  144 + if Kernel.system("cd #{File.dirname(project.second)} > /dev/null 2>&1 && git clone --bare #{backup_path_repo}/#{project.first}.bundle #{project.first}.git > /dev/null 2>&1")
  145 + puts "[DONE]".green
  146 + else
  147 + puts "[FAILED]".red
  148 + end
  149 + end
  150 + end
  151 +
  152 + ###################################### DB ######################################
  153 +
  154 + task :db_dump => :environment do
  155 + backup_path_db = File.join(Gitlab.config.backup_path, "db")
  156 + FileUtils.mkdir_p(backup_path_db) until Dir.exists?(backup_path_db)
  157 + puts "Dumping database tables:"
  158 + ActiveRecord::Base.connection.tables.each do |tbl|
  159 + print "- Dumping table #{tbl}... "
  160 + count = 1
  161 + File.open(File.join(backup_path_db, tbl + ".yml"), "w+") do |file|
  162 + ActiveRecord::Base.connection.select_all("SELECT * FROM `#{tbl}`").each do |line|
  163 + line.delete_if{|k,v| v.blank?}
  164 + output = {tbl + '_' + count.to_s => line}
  165 + file << output.to_yaml.gsub(/^---\n/,'') + "\n"
  166 + count += 1
  167 + end
  168 + puts "[DONE]".green
  169 + end
  170 + end
  171 + end
  172 +
  173 + task :db_restore=> :environment do
  174 + backup_path_db = File.join(Gitlab.config.backup_path, "db")
  175 + puts "Restoring database tables:"
  176 + Rake::Task["db:reset"].invoke
  177 + Dir.glob(File.join(backup_path_db, "*.yml") ).each do |dir|
  178 + fixture_file = File.basename(dir, ".*" )
  179 + print "- Loading fixture #{fixture_file}..."
  180 + if File.size(dir) > 0
  181 + ActiveRecord::Fixtures.create_fixtures(backup_path_db, fixture_file)
  182 + puts "[DONE]".green
  183 + else
  184 + puts "[SKIPPING]".yellow
  185 + end
  186 + end
  187 + end
  188 +
  189 + end # namespace end: app
  190 +end # namespace end: gitlab
... ...
resque.sh
1 1 mkdir -p tmp/pids
2   -bundle exec rake environment resque:work QUEUE=post_receive,mailer RAILS_ENV=production PIDFILE=tmp/pids/resque_worker.pid BACKGROUND=yes
  2 +bundle exec rake environment resque:work QUEUE=post_receive,mailer,system_hook RAILS_ENV=production PIDFILE=tmp/pids/resque_worker.pid BACKGROUND=yes
... ...
resque_dev.sh
1   -bundle exec rake environment resque:work QUEUE=* VVERBOSE=1
  1 +bundle exec rake environment resque:work QUEUE=post_receive,mailer,system_hook VVERBOSE=1
... ...
spec/api/projects_spec.rb
... ... @@ -78,7 +78,7 @@ describe Gitlab::API do
78 78 end
79 79  
80 80 describe "DELETE /projects/:id/snippets/:snippet_id" do
81   - it "should create a new project snippet" do
  81 + it "should delete existing project snippet" do
82 82 expect {
83 83 delete "#{api_prefix}/projects/#{project.code}/snippets/#{snippet.id}?private_token=#{user.private_token}"
84 84 }.should change { Snippet.count }.by(-1)
... ...
spec/factories.rb
... ... @@ -7,6 +7,12 @@ Factory.add(:project, Project) do |obj|
7 7 obj.code = 'LGT'
8 8 end
9 9  
  10 +Factory.add(:project_without_owner, Project) do |obj|
  11 + obj.name = Faker::Internet.user_name
  12 + obj.path = 'gitlabhq'
  13 + obj.code = 'LGT'
  14 +end
  15 +
10 16 Factory.add(:public_project, Project) do |obj|
11 17 obj.name = Faker::Internet.user_name
12 18 obj.path = 'gitlabhq'
... ... @@ -60,7 +66,11 @@ Factory.add(:key, Key) do |obj|
60 66 obj.key = File.read(File.join(Rails.root, "db", "pkey.example"))
61 67 end
62 68  
63   -Factory.add(:web_hook, WebHook) do |obj|
  69 +Factory.add(:project_hook, ProjectHook) do |obj|
  70 + obj.url = Faker::Internet.uri("http")
  71 +end
  72 +
  73 +Factory.add(:system_hook, SystemHook) do |obj|
64 74 obj.url = Faker::Internet.uri("http")
65 75 end
66 76  
... ...
spec/models/merge_request_spec.rb
... ... @@ -13,7 +13,6 @@ describe MergeRequest do
13 13 it { should validate_presence_of(:title) }
14 14 it { should validate_presence_of(:author_id) }
15 15 it { should validate_presence_of(:project_id) }
16   - it { should validate_presence_of(:assignee_id) }
17 16 end
18 17  
19 18 describe "Scope" do
... ...
spec/models/project_hooks_spec.rb
... ... @@ -21,44 +21,44 @@ describe Project, &quot;Hooks&quot; do
21 21 end
22 22 end
23 23  
24   - describe "Web hooks" do
  24 + describe "Project hooks" do
25 25 context "with no web hooks" do
26 26 it "raises no errors" do
27 27 lambda {
28   - project.execute_web_hooks('oldrev', 'newrev', 'ref', @user)
  28 + project.execute_hooks('oldrev', 'newrev', 'ref', @user)
29 29 }.should_not raise_error
30 30 end
31 31 end
32 32  
33 33 context "with web hooks" do
34 34 before do
35   - @webhook = Factory(:web_hook)
36   - @webhook_2 = Factory(:web_hook)
37   - project.web_hooks << [@webhook, @webhook_2]
  35 + @project_hook = Factory(:project_hook)
  36 + @project_hook_2 = Factory(:project_hook)
  37 + project.hooks << [@project_hook, @project_hook_2]
38 38 end
39 39  
40 40 it "executes multiple web hook" do
41   - @webhook.should_receive(:execute).once
42   - @webhook_2.should_receive(:execute).once
  41 + @project_hook.should_receive(:execute).once
  42 + @project_hook_2.should_receive(:execute).once
43 43  
44   - project.execute_web_hooks('oldrev', 'newrev', 'refs/heads/master', @user)
  44 + project.execute_hooks('oldrev', 'newrev', 'refs/heads/master', @user)
45 45 end
46 46 end
47 47  
48 48 context "does not execute web hooks" do
49 49 before do
50   - @webhook = Factory(:web_hook)
51   - project.web_hooks << [@webhook]
  50 + @project_hook = Factory(:project_hook)
  51 + project.hooks << [@project_hook]
52 52 end
53 53  
54 54 it "when pushing a branch for the first time" do
55   - @webhook.should_not_receive(:execute)
56   - project.execute_web_hooks('00000000000000000000000000000000', 'newrev', 'refs/heads/master', @user)
  55 + @project_hook.should_not_receive(:execute)
  56 + project.execute_hooks('00000000000000000000000000000000', 'newrev', 'refs/heads/master', @user)
57 57 end
58 58  
59 59 it "when pushing tags" do
60   - @webhook.should_not_receive(:execute)
61   - project.execute_web_hooks('oldrev', 'newrev', 'refs/tags/v1.0.0', @user)
  60 + @project_hook.should_not_receive(:execute)
  61 + project.execute_hooks('oldrev', 'newrev', 'refs/tags/v1.0.0', @user)
62 62 end
63 63 end
64 64  
... ...
spec/models/project_spec.rb
... ... @@ -11,7 +11,7 @@ describe Project do
11 11 it { should have_many(:issues).dependent(:destroy) }
12 12 it { should have_many(:notes).dependent(:destroy) }
13 13 it { should have_many(:snippets).dependent(:destroy) }
14   - it { should have_many(:web_hooks).dependent(:destroy) }
  14 + it { should have_many(:hooks).dependent(:destroy) }
15 15 it { should have_many(:deploy_keys).dependent(:destroy) }
16 16 end
17 17  
... ...
spec/models/system_hook_spec.rb 0 → 100644
... ... @@ -0,0 +1,63 @@
  1 +require "spec_helper"
  2 +
  3 +describe SystemHook do
  4 + describe "execute" do
  5 + before(:each) { ActiveRecord::Base.observers.enable(:all) }
  6 +
  7 + before(:each) do
  8 + @system_hook = Factory :system_hook
  9 + WebMock.stub_request(:post, @system_hook.url)
  10 + end
  11 +
  12 + it "project_create hook" do
  13 + user = Factory :user
  14 + with_resque do
  15 + project = Factory :project_without_owner, :owner => user
  16 + end
  17 + WebMock.should have_requested(:post, @system_hook.url).with(body: /project_create/).once
  18 + end
  19 +
  20 + it "project_destroy hook" do
  21 + project = Factory :project
  22 + with_resque do
  23 + project.destroy
  24 + end
  25 + WebMock.should have_requested(:post, @system_hook.url).with(body: /project_destroy/).once
  26 + end
  27 +
  28 + it "user_create hook" do
  29 + with_resque do
  30 + Factory :user
  31 + end
  32 + WebMock.should have_requested(:post, @system_hook.url).with(body: /user_create/).once
  33 + end
  34 +
  35 + it "user_destroy hook" do
  36 + user = Factory :user
  37 + with_resque do
  38 + user.destroy
  39 + end
  40 + WebMock.should have_requested(:post, @system_hook.url).with(body: /user_destroy/).once
  41 + end
  42 +
  43 + it "project_create hook" do
  44 + user = Factory :user
  45 + project = Factory :project
  46 + with_resque do
  47 + project.users << user
  48 + end
  49 + WebMock.should have_requested(:post, @system_hook.url).with(body: /user_add_to_team/).once
  50 + end
  51 +
  52 + it "project_destroy hook" do
  53 + user = Factory :user
  54 + project = Factory :project
  55 + project.users << user
  56 + with_resque do
  57 + project.users_projects.clear
  58 + end
  59 + WebMock.should have_requested(:post, @system_hook.url).with(body: /user_remove_from_team/).once
  60 + end
  61 + end
  62 +
  63 +end
... ...
spec/models/web_hook_spec.rb
1 1 require 'spec_helper'
2 2  
3   -describe WebHook do
  3 +describe ProjectHook do
4 4 describe "Associations" do
5 5 it { should belong_to :project }
6 6 end
... ... @@ -23,32 +23,32 @@ describe WebHook do
23 23  
24 24 describe "execute" do
25 25 before(:each) do
26   - @webhook = Factory :web_hook
  26 + @project_hook = Factory :project_hook
27 27 @project = Factory :project
28   - @project.web_hooks << [@webhook]
  28 + @project.hooks << [@project_hook]
29 29 @data = { before: 'oldrev', after: 'newrev', ref: 'ref'}
30 30  
31   - WebMock.stub_request(:post, @webhook.url)
  31 + WebMock.stub_request(:post, @project_hook.url)
32 32 end
33 33  
34 34 it "POSTs to the web hook URL" do
35   - @webhook.execute(@data)
36   - WebMock.should have_requested(:post, @webhook.url).once
  35 + @project_hook.execute(@data)
  36 + WebMock.should have_requested(:post, @project_hook.url).once
37 37 end
38 38  
39 39 it "POSTs the data as JSON" do
40 40 json = @data.to_json
41 41  
42   - @webhook.execute(@data)
43   - WebMock.should have_requested(:post, @webhook.url).with(body: json).once
  42 + @project_hook.execute(@data)
  43 + WebMock.should have_requested(:post, @project_hook.url).with(body: json).once
44 44 end
45 45  
46 46 it "catches exceptions" do
47 47 WebHook.should_receive(:post).and_raise("Some HTTP Post error")
48 48  
49 49 lambda {
50   - @webhook.execute(@data)
51   - }.should_not raise_error
  50 + @project_hook.execute(@data)
  51 + }.should raise_error
52 52 end
53 53 end
54 54 end
... ...
spec/requests/admin/admin_hooks_spec.rb 0 → 100644
... ... @@ -0,0 +1,53 @@
  1 +require 'spec_helper'
  2 +
  3 +describe "Admin::Hooks" do
  4 + before do
  5 + @project = Factory :project,
  6 + :name => "LeGiT",
  7 + :code => "LGT"
  8 + login_as :admin
  9 +
  10 + @system_hook = Factory :system_hook
  11 +
  12 + end
  13 +
  14 + describe "GET /admin/hooks" do
  15 + it "should be ok" do
  16 + visit admin_root_path
  17 + within ".main_menu" do
  18 + click_on "Hooks"
  19 + end
  20 + current_path.should == admin_hooks_path
  21 + end
  22 +
  23 + it "should have hooks list" do
  24 + visit admin_hooks_path
  25 + page.should have_content(@system_hook.url)
  26 + end
  27 + end
  28 +
  29 + describe "New Hook" do
  30 + before do
  31 + @url = Faker::Internet.uri("http")
  32 + visit admin_hooks_path
  33 + fill_in "hook_url", :with => @url
  34 + expect { click_button "Add System Hook" }.to change(SystemHook, :count).by(1)
  35 + end
  36 +
  37 + it "should open new hook popup" do
  38 + page.current_path.should == admin_hooks_path
  39 + page.should have_content(@url)
  40 + end
  41 + end
  42 +
  43 + describe "Test" do
  44 + before do
  45 + WebMock.stub_request(:post, @system_hook.url)
  46 + visit admin_hooks_path
  47 + click_link "Test Hook"
  48 + end
  49 +
  50 + it { page.current_path.should == admin_hooks_path }
  51 + end
  52 +
  53 +end
... ...
spec/requests/admin/security_spec.rb
... ... @@ -13,9 +13,9 @@ describe &quot;Admin::Projects&quot; do
13 13 it { admin_users_path.should be_denied_for :visitor }
14 14 end
15 15  
16   - describe "GET /admin/emails" do
17   - it { admin_emails_path.should be_allowed_for :admin }
18   - it { admin_emails_path.should be_denied_for :user }
19   - it { admin_emails_path.should be_denied_for :visitor }
  16 + describe "GET /admin/hooks" do
  17 + it { admin_hooks_path.should be_allowed_for :admin }
  18 + it { admin_hooks_path.should be_denied_for :user }
  19 + it { admin_hooks_path.should be_denied_for :visitor }
20 20 end
21 21 end
... ...
spec/requests/hooks_spec.rb
... ... @@ -9,7 +9,7 @@ describe &quot;Hooks&quot; do
9 9  
10 10 describe "GET index" do
11 11 it "should be available" do
12   - @hook = Factory :web_hook, :project => @project
  12 + @hook = Factory :project_hook, :project => @project
13 13 visit project_hooks_path(@project)
14 14 page.should have_content "Hooks"
15 15 page.should have_content @hook.url
... ... @@ -21,7 +21,7 @@ describe &quot;Hooks&quot; do
21 21 @url = Faker::Internet.uri("http")
22 22 visit project_hooks_path(@project)
23 23 fill_in "hook_url", :with => @url
24   - expect { click_button "Add Web Hook" }.to change(WebHook, :count).by(1)
  24 + expect { click_button "Add Web Hook" }.to change(ProjectHook, :count).by(1)
25 25 end
26 26  
27 27 it "should open new team member popup" do
... ... @@ -32,7 +32,8 @@ describe &quot;Hooks&quot; do
32 32  
33 33 describe "Test" do
34 34 before do
35   - @hook = Factory :web_hook, :project => @project
  35 + @hook = Factory :project_hook, :project => @project
  36 + stub_request(:post, @hook.url)
36 37 visit project_hooks_path(@project)
37 38 click_link "Test Hook"
38 39 end
... ...
spec/workers/post_receive_spec.rb
... ... @@ -22,14 +22,14 @@ describe PostReceive do
22 22 Key.stub(find_by_identifier: nil)
23 23  
24 24 project.should_not_receive(:observe_push)
25   - project.should_not_receive(:execute_web_hooks)
  25 + project.should_not_receive(:execute_hooks)
26 26  
27 27 PostReceive.perform(project.path, 'sha-old', 'sha-new', 'refs/heads/master', key_id).should be_false
28 28 end
29 29  
30 30 it "asks the project to execute web hooks" do
31 31 Project.stub(find_by_path: project)
32   - project.should_receive(:execute_web_hooks).with('sha-old', 'sha-new', 'refs/heads/master', project.owner)
  32 + project.should_receive(:execute_hooks).with('sha-old', 'sha-new', 'refs/heads/master', project.owner)
33 33  
34 34 PostReceive.perform(project.path, 'sha-old', 'sha-new', 'refs/heads/master', key_id)
35 35 end
... ...
vendor/assets/javascripts/jquery.waitforimages.js 0 → 100644
... ... @@ -0,0 +1,144 @@
  1 +/*
  2 + * waitForImages 1.4
  3 + * -----------------
  4 + * Provides a callback when all images have loaded in your given selector.
  5 + * http://www.alexanderdickson.com/
  6 + *
  7 + *
  8 + * Copyright (c) 2011 Alex Dickson
  9 + * Licensed under the MIT licenses.
  10 + * See website for more info.
  11 + *
  12 + */
  13 +
  14 +;(function($) {
  15 + // Namespace all events.
  16 + var eventNamespace = 'waitForImages';
  17 +
  18 + // CSS properties which contain references to images.
  19 + $.waitForImages = {
  20 + hasImageProperties: [
  21 + 'backgroundImage',
  22 + 'listStyleImage',
  23 + 'borderImage',
  24 + 'borderCornerImage'
  25 + ]
  26 + };
  27 +
  28 + // Custom selector to find `img` elements that have a valid `src` attribute and have not already loaded.
  29 + $.expr[':'].uncached = function(obj) {
  30 + // Ensure we are dealing with an `img` element with a valid `src` attribute.
  31 + if ( ! $(obj).is('img[src!=""]')) {
  32 + return false;
  33 + }
  34 +
  35 + // Firefox's `complete` property will always be`true` even if the image has not been downloaded.
  36 + // Doing it this way works in Firefox.
  37 + var img = document.createElement('img');
  38 + img.src = obj.src;
  39 + return ! img.complete;
  40 + };
  41 +
  42 + $.fn.waitForImages = function(finishedCallback, eachCallback, waitForAll) {
  43 +
  44 + // Handle options object.
  45 + if ($.isPlainObject(arguments[0])) {
  46 + eachCallback = finishedCallback.each;
  47 + waitForAll = finishedCallback.waitForAll;
  48 + finishedCallback = finishedCallback.finished;
  49 + }
  50 +
  51 + // Handle missing callbacks.
  52 + finishedCallback = finishedCallback || $.noop;
  53 + eachCallback = eachCallback || $.noop;
  54 +
  55 + // Convert waitForAll to Boolean
  56 + waitForAll = !! waitForAll;
  57 +
  58 + // Ensure callbacks are functions.
  59 + if (!$.isFunction(finishedCallback) || !$.isFunction(eachCallback)) {
  60 + throw new TypeError('An invalid callback was supplied.');
  61 + };
  62 +
  63 + return this.each(function() {
  64 + // Build a list of all imgs, dependent on what images will be considered.
  65 + var obj = $(this),
  66 + allImgs = [];
  67 +
  68 + if (waitForAll) {
  69 + // CSS properties which may contain an image.
  70 + var hasImgProperties = $.waitForImages.hasImageProperties || [],
  71 + matchUrl = /url\((['"]?)(.*?)\1\)/g;
  72 +
  73 + // Get all elements, as any one of them could have a background image.
  74 + obj.find('*').each(function() {
  75 + var element = $(this);
  76 +
  77 + // If an `img` element, add it. But keep iterating in case it has a background image too.
  78 + if (element.is('img:uncached')) {
  79 + allImgs.push({
  80 + src: element.attr('src'),
  81 + element: element[0]
  82 + });
  83 + }
  84 +
  85 + $.each(hasImgProperties, function(i, property) {
  86 + var propertyValue = element.css(property);
  87 + // If it doesn't contain this property, skip.
  88 + if ( ! propertyValue) {
  89 + return true;
  90 + }
  91 +
  92 + // Get all url() of this element.
  93 + var match;
  94 + while (match = matchUrl.exec(propertyValue)) {
  95 + allImgs.push({
  96 + src: match[2],
  97 + element: element[0]
  98 + });
  99 + };
  100 + });
  101 + });
  102 + } else {
  103 + // For images only, the task is simpler.
  104 + obj
  105 + .find('img:uncached')
  106 + .each(function() {
  107 + allImgs.push({
  108 + src: this.src,
  109 + element: this
  110 + });
  111 + });
  112 + };
  113 +
  114 + var allImgsLength = allImgs.length,
  115 + allImgsLoaded = 0;
  116 +
  117 + // If no images found, don't bother.
  118 + if (allImgsLength == 0) {
  119 + finishedCallback.call(obj[0]);
  120 + };
  121 +
  122 + $.each(allImgs, function(i, img) {
  123 +
  124 + var image = new Image;
  125 +
  126 + // Handle the image loading and error with the same callback.
  127 + $(image).bind('load.' + eventNamespace + ' error.' + eventNamespace, function(event) {
  128 + allImgsLoaded++;
  129 +
  130 + // If an error occurred with loading the image, set the third argument accordingly.
  131 + eachCallback.call(img.element, allImgsLoaded, allImgsLength, event.type == 'load');
  132 +
  133 + if (allImgsLoaded == allImgsLength) {
  134 + finishedCallback.call(obj[0]);
  135 + return false;
  136 + };
  137 +
  138 + });
  139 +
  140 + image.src = img.src;
  141 + });
  142 + });
  143 + };
  144 +})(jQuery);
... ...