Commit a0af55302055ad839e6bd2cd838b0f5c16092780

Authored by Andrey Subbota
2 parents 729b2c1b 30b7e5b6
Exists in master and in 1 other branch production

Merge branch 'errbit' into feature/upstream_merge

Showing 135 changed files with 571 additions and 292 deletions   Show diff stats
Gemfile
... ... @@ -4,7 +4,6 @@ gem 'rails', '3.2.8'
4 4 gem 'mongoid', '~> 2.4.10'
5 5 gem 'mongoid_rails_migrations'
6 6 gem 'devise', '~> 1.5.3'
7   -gem 'nokogiri'
8 7 gem 'haml'
9 8 gem 'htmlentities', "~> 4.3.0"
10 9 gem 'rack-ssl', :require => 'rack/ssl' # force SSL
... ... @@ -37,6 +36,8 @@ gem 'pivotal-tracker'
37 36 gem 'ruby-fogbugz', :require => 'fogbugz'
38 37 # Github Issues
39 38 gem 'octokit', '~> 1.0.0'
  39 +# Gitlab
  40 +gem 'gitlab'
40 41  
41 42 # Bitbucket Issues
42 43 gem 'bitbucket_rest_api'
... ... @@ -66,8 +67,6 @@ platform :ruby do
66 67 gem 'bson_ext', '= 1.6.2'
67 68 end
68 69  
69   -gem 'omniauth'
70   -gem 'oa-core'
71 70 gem 'ri_cal'
72 71 gem 'yajl-ruby', :require => "yajl"
73 72  
... ... @@ -77,25 +76,22 @@ group :development, :test do
77 76 unless ENV["CI"]
78 77 gem 'ruby-debug', :platform => :mri_18
79 78 gem 'debugger', :platform => :mri_19
80   - gem 'pry'
81 79 gem 'pry-rails'
82 80 end
83 81 # gem 'rpm_contrib'
84 82 # gem 'newrelic_rpm'
85 83 gem 'capistrano'
86   - gem 'capistrano_colors'
87 84 end
88 85  
89 86 group :test do
90 87 gem 'capybara'
91 88 gem 'launchy'
92   - gem 'rspec', '~> 2.6'
93 89 gem 'database_cleaner', '~> 0.6.0'
94 90 gem 'email_spec'
95 91 gem 'timecop'
96 92 end
97 93  
98   -group :heroku do
  94 +group :heroku, :production do
99 95 gem 'unicorn'
100 96 end
101 97  
... ... @@ -108,6 +104,7 @@ group :assets do
108 104 gem 'execjs'
109 105 gem 'therubyracer', :platform => :ruby # C Ruby (MRI) or Rubinius, but NOT Windows
110 106 gem 'uglifier', '>= 1.0.3'
  107 + gem 'underscore-rails'
111 108 end
112 109  
113 110 gem 'turbo-sprockets-rails3'
... ...
Gemfile.lock
... ... @@ -49,13 +49,12 @@ GEM
49 49 builder (3.0.4)
50 50 campy (0.1.3)
51 51 multi_json (~> 1.0)
52   - capistrano (2.13.4)
  52 + capistrano (2.13.5)
53 53 highline
54 54 net-scp (>= 1.0.0)
55 55 net-sftp (>= 2.0.0)
56 56 net-ssh (>= 2.0.14)
57 57 net-ssh-gateway (>= 1.1.0)
58   - capistrano_colors (0.5.5)
59 58 capybara (1.1.2)
60 59 mime-types (>= 1.16)
61 60 nokogiri (>= 1.3.3)
... ... @@ -73,13 +72,13 @@ GEM
73 72 rdoc
74 73 daemons (1.1.8)
75 74 database_cleaner (0.6.7)
76   - debugger (1.2.0)
  75 + debugger (1.2.1)
77 76 columnize (>= 0.3.1)
78 77 debugger-linecache (~> 1.1.1)
79   - debugger-ruby_core_source (~> 1.1.3)
  78 + debugger-ruby_core_source (~> 1.1.4)
80 79 debugger-linecache (1.1.2)
81 80 debugger-ruby_core_source (>= 1.1.1)
82   - debugger-ruby_core_source (1.1.3)
  81 + debugger-ruby_core_source (1.1.4)
83 82 devise (1.5.3)
84 83 bcrypt-ruby (~> 3.0)
85 84 orm_adapter (~> 0.0.3)
... ... @@ -169,7 +168,6 @@ GEM
169 168 net-ssh-gateway (1.1.0)
170 169 net-ssh (>= 1.99.1)
171 170 nokogiri (1.5.5)
172   - oa-core (0.3.2)
173 171 oauth2 (0.8.0)
174 172 faraday (~> 0.8)
175 173 httpauth (~> 0.1)
... ... @@ -295,13 +293,14 @@ GEM
295 293 treetop (1.4.10)
296 294 polyglot
297 295 polyglot (>= 0.3.1)
298   - turbo-sprockets-rails3 (0.1.10)
299   - railties (>= 3.1.0)
  296 + turbo-sprockets-rails3 (0.2.12)
  297 + railties (>= 3.1.0, < 3.2.9)
300 298 sprockets (>= 2.0.0)
301 299 tzinfo (0.3.33)
302 300 uglifier (1.2.7)
303 301 execjs (>= 0.3.0)
304 302 multi_json (~> 1.3)
  303 + underscore-rails (1.4.2.1)
305 304 unicorn (4.3.1)
306 305 kgio (~> 2.6)
307 306 rack
... ... @@ -328,7 +327,6 @@ DEPENDENCIES
328 327 bson_ext (= 1.6.2)
329 328 campy
330 329 capistrano
331   - capistrano_colors
332 330 capybara
333 331 database_cleaner (~> 0.6.0)
334 332 debugger
... ... @@ -348,21 +346,16 @@ DEPENDENCIES
348 346 mongo (= 1.6.2)
349 347 mongoid (~> 2.4.10)
350 348 mongoid_rails_migrations
351   - nokogiri
352   - oa-core
353 349 octokit (~> 1.0.0)
354   - omniauth
355 350 omniauth-github
356 351 oruen_redmine_client
357 352 pivotal-tracker
358   - pry
359 353 pry-rails
360 354 rack-ssl
361 355 rack-ssl-enforcer
362 356 rails (= 3.2.8)
363 357 rails_autolink (~> 1.0.9)
364 358 ri_cal
365   - rspec (~> 2.6)
366 359 rspec-rails (~> 2.6)
367 360 ruby-debug
368 361 ruby-fogbugz
... ... @@ -372,6 +365,7 @@ DEPENDENCIES
372 365 timecop
373 366 turbo-sprockets-rails3
374 367 uglifier (>= 1.0.3)
  368 + underscore-rails
375 369 unicorn
376 370 useragent (~> 0.3.1)
377 371 webmock
... ...
README.md
... ... @@ -311,6 +311,7 @@ When upgrading Errbit, please run:
311 311  
312 312 ```bash
313 313 git pull origin master # assuming origin is the github.com/errbit/errbit repo
  314 +bundle install
314 315 rake db:migrate
315 316 ```
316 317  
... ... @@ -376,12 +377,19 @@ card_type = Defect, status = Open, priority = Essential
376 377 * For 'Account/Repository', the account will either be a username or organization. i.e. **errbit/errbit**
377 378 * You will also need to provide your username and password for your GitHub account.
378 379 * (We'd really appreciate it if you wanted to help us implement OAuth instead!)
379   -
  380 +
380 381 **Bitbucket Issues Integration**
381 382  
382 383 * For 'BITBUCKET REPO' field, the account will either be a username or organization. i.e. **errbit/errbit**
383 384 * You will also need to provide your username and password for your Bitbucket account.
384 385  
  386 +**Gitlab Issues Integration**
  387 +
  388 +* Account is the host of your gitlab installation. i.e. **http://gitlab.example.com**
  389 +* To authenticate, Errbit uses token-based authentication. Get your API Key in your user settings (or create special user for this purpose)
  390 +* You also need to provide project name (shortname) or ID (number) for issues to be created
  391 +* **Currently (as of 3.0), Gitlab has 2000 character limit for issue description.** It is necessary to turn it off at your instance, because Errbit issues body is much longer. Please comment validation line in issue model in models folder https://github.com/gitlabhq/gitlabhq/blob/master/app/models/issue.rb#L10
  392 +
385 393  
386 394  
387 395 What if Errbit has an error?
... ... @@ -436,10 +444,15 @@ Special Thanks
436 444 --------------
437 445  
438 446 * [Michael Parenteau](http://michaelparenteau.com) - For rocking the Errbit design and providing a great user experience.
439   -* [Nick Recobra aka oruen](https://github.com/oruen) - Nick is Errbit's first core contributor. He's been working hard at making Errbit more awesome.
  447 +* [Nick Recobra (@oruen)](https://github.com/oruen) - Nick is Errbit's first core contributor. He's been working hard at making Errbit more awesome.
  448 +* [Nathan Broadbent (@ndbroadbent)](https://github.com/ndbroadbent) - Maintaining Errbit and contributing many features
  449 +* [Vasiliy Ermolovich (@nashby)](https://github.com/nashby) - Contributing and helping to resolve issues and pull requests
  450 +* [Marcin Ciunelis (@martinciu)](https://github.com/martinciu) - Helping to improve Errbit's architecture
440 451 * [Relevance](http://thinkrelevance.com) - For giving me Open-source Fridays to work on Errbit and all my awesome co-workers for giving feedback and inspiration.
441 452 * [Thoughtbot](http://thoughtbot.com) - For being great open-source advocates and setting the bar with [Airbrake](http://airbrakeapp.com).
442 453  
  454 +See the [contributors graph](https://github.com/errbit/errbit/graphs/contributors) for further details.
  455 +
443 456  
444 457 Contributing
445 458 ------------
... ...
app/assets/images/alerts/help.gif

1.54 KB | W: | H:

1.53 KB | W: | H:

  • 2-up
  • Swipe
  • Onion skin
app/assets/images/alerts/important.gif

1.46 KB | W: | H:

1.45 KB | W: | H:

  • 2-up
  • Swipe
  • Onion skin
app/assets/images/alerts/info.gif

1.45 KB | W: | H:

1.44 KB | W: | H:

  • 2-up
  • Swipe
  • Onion skin
app/assets/images/alerts/title.gif

317 Bytes | W: | H:

309 Bytes | W: | H:

  • 2-up
  • Swipe
  • Onion skin
app/assets/images/bitbucket_create.png

2.54 KB | W: | H:

2.43 KB | W: | H:

  • 2-up
  • Swipe
  • Onion skin
app/assets/images/bitbucket_goto.png

3.61 KB | W: | H:

2.45 KB | W: | H:

  • 2-up
  • Swipe
  • Onion skin
app/assets/images/bitbucket_inactive.png

1.95 KB | W: | H:

1.23 KB | W: | H:

  • 2-up
  • Swipe
  • Onion skin
app/assets/images/campfire_create.png

3.19 KB | W: | H:

2.03 KB | W: | H:

  • 2-up
  • Swipe
  • Onion skin
app/assets/images/campfire_goto.png

3.19 KB | W: | H:

2.03 KB | W: | H:

  • 2-up
  • Swipe
  • Onion skin
app/assets/images/campfire_inactive.png

2.8 KB | W: | H:

1.58 KB | W: | H:

  • 2-up
  • Swipe
  • Onion skin
app/assets/images/fogbugz_create.png

4.78 KB | W: | H:

1.89 KB | W: | H:

  • 2-up
  • Swipe
  • Onion skin
app/assets/images/fogbugz_goto.png

4.78 KB | W: | H:

1.89 KB | W: | H:

  • 2-up
  • Swipe
  • Onion skin
app/assets/images/fogbugz_inactive.png

4.58 KB | W: | H:

1.04 KB | W: | H:

  • 2-up
  • Swipe
  • Onion skin
app/assets/images/github_create.png

2.3 KB | W: | H:

2.14 KB | W: | H:

  • 2-up
  • Swipe
  • Onion skin
app/assets/images/github_goto.png

2.3 KB | W: | H:

2.14 KB | W: | H:

  • 2-up
  • Swipe
  • Onion skin
app/assets/images/github_inactive.png

2.03 KB | W: | H:

1.05 KB | W: | H:

  • 2-up
  • Swipe
  • Onion skin
app/assets/images/gitlab_create.png 0 → 100644

4.47 KB

app/assets/images/gitlab_goto.png 0 → 100644

4.47 KB

app/assets/images/gitlab_inactive.png 0 → 100644

4.35 KB

app/assets/images/gtalk_create.png

4.62 KB | W: | H:

1.71 KB | W: | H:

  • 2-up
  • Swipe
  • Onion skin
app/assets/images/gtalk_goto.png

4.62 KB | W: | H:

1.71 KB | W: | H:

  • 2-up
  • Swipe
  • Onion skin
app/assets/images/gtalk_inactive.png

4.07 KB | W: | H:

1.28 KB | W: | H:

  • 2-up
  • Swipe
  • Onion skin
app/assets/images/hipchat_create.png

2.05 KB | W: | H:

1.59 KB | W: | H:

  • 2-up
  • Swipe
  • Onion skin
app/assets/images/hipchat_goto.png

2.05 KB | W: | H:

1.59 KB | W: | H:

  • 2-up
  • Swipe
  • Onion skin
app/assets/images/hipchat_inactive.png

1.16 KB | W: | H:

904 Bytes | W: | H:

  • 2-up
  • Swipe
  • Onion skin
app/assets/images/hoiio_create.png

1.72 KB | W: | H:

1.52 KB | W: | H:

  • 2-up
  • Swipe
  • Onion skin
app/assets/images/hoiio_goto.png

1.72 KB | W: | H:

1.52 KB | W: | H:

  • 2-up
  • Swipe
  • Onion skin
app/assets/images/hoiio_inactive.png

874 Bytes | W: | H:

783 Bytes | W: | H:

  • 2-up
  • Swipe
  • Onion skin
app/assets/images/lighthouseapp_create.png

1.57 KB | W: | H:

1.38 KB | W: | H:

  • 2-up
  • Swipe
  • Onion skin
app/assets/images/lighthouseapp_goto.png

1.61 KB | W: | H:

1.4 KB | W: | H:

  • 2-up
  • Swipe
  • Onion skin
app/assets/images/lighthouseapp_inactive.png

1.57 KB | W: | H:

1.16 KB | W: | H:

  • 2-up
  • Swipe
  • Onion skin
app/assets/images/loader.gif

1.7 KB | W: | H:

1.6 KB | W: | H:

  • 2-up
  • Swipe
  • Onion skin
app/assets/images/mingle_create.png

2.41 KB | W: | H:

2.13 KB | W: | H:

  • 2-up
  • Swipe
  • Onion skin
app/assets/images/mingle_goto.png

2.41 KB | W: | H:

2.13 KB | W: | H:

  • 2-up
  • Swipe
  • Onion skin
app/assets/images/mingle_inactive.png

1.96 KB | W: | H:

1.38 KB | W: | H:

  • 2-up
  • Swipe
  • Onion skin
app/assets/images/none_create.png

799 Bytes | W: | H:

548 Bytes | W: | H:

  • 2-up
  • Swipe
  • Onion skin
app/assets/images/none_inactive.png

818 Bytes | W: | H:

558 Bytes | W: | H:

  • 2-up
  • Swipe
  • Onion skin
app/assets/images/pivotal_create.png

1.5 KB | W: | H:

1.26 KB | W: | H:

  • 2-up
  • Swipe
  • Onion skin
app/assets/images/pivotal_goto.png

1.52 KB | W: | H:

1.29 KB | W: | H:

  • 2-up
  • Swipe
  • Onion skin
app/assets/images/pivotal_inactive.png

1.56 KB | W: | H:

1.1 KB | W: | H:

  • 2-up
  • Swipe
  • Onion skin
app/assets/images/pushover_create.png

1.91 KB | W: | H:

1.56 KB | W: | H:

  • 2-up
  • Swipe
  • Onion skin
app/assets/images/pushover_goto.png

1.91 KB | W: | H:

1.56 KB | W: | H:

  • 2-up
  • Swipe
  • Onion skin
app/assets/images/pushover_inactive.png

1.02 KB | W: | H:

898 Bytes | W: | H:

  • 2-up
  • Swipe
  • Onion skin
app/assets/images/redmine_create.png

1.47 KB | W: | H:

1.33 KB | W: | H:

  • 2-up
  • Swipe
  • Onion skin
app/assets/images/redmine_goto.png

1.52 KB | W: | H:

1.35 KB | W: | H:

  • 2-up
  • Swipe
  • Onion skin
app/assets/images/redmine_inactive.png

1.52 KB | W: | H:

1.13 KB | W: | H:

  • 2-up
  • Swipe
  • Onion skin
app/assets/images/thumbs-up.png

1.42 KB | W: | H:

1.28 KB | W: | H:

  • 2-up
  • Swipe
  • Onion skin
app/assets/javascripts/application.js.erb
1 1 //= require jquery
2   -//= require underscore-1.1.6
  2 +//= require underscore
3 3 //= require rails
4 4 //= require form
5 5 //= require jquery.pjax
... ...
app/assets/javascripts/underscore-1.1.6.js
... ... @@ -1,26 +0,0 @@
1   -// Underscore.js 1.1.6
2   -// (c) 2011 Jeremy Ashkenas, DocumentCloud Inc.
3   -// Underscore is freely distributable under the MIT license.
4   -// Portions of Underscore are inspired or borrowed from Prototype,
5   -// Oliver Steele's Functional, and John Resig's Micro-Templating.
6   -// For all details and documentation:
7   -// http://documentcloud.github.com/underscore
8   -(function(){var p=this,C=p._,m={},i=Array.prototype,n=Object.prototype,f=i.slice,D=i.unshift,E=n.toString,l=n.hasOwnProperty,s=i.forEach,t=i.map,u=i.reduce,v=i.reduceRight,w=i.filter,x=i.every,y=i.some,o=i.indexOf,z=i.lastIndexOf;n=Array.isArray;var F=Object.keys,q=Function.prototype.bind,b=function(a){return new j(a)};typeof module!=="undefined"&&module.exports?(module.exports=b,b._=b):p._=b;b.VERSION="1.1.6";var h=b.each=b.forEach=function(a,c,d){if(a!=null)if(s&&a.forEach===s)a.forEach(c,d);else if(b.isNumber(a.length))for(var e=
9   -0,k=a.length;e<k;e++){if(c.call(d,a[e],e,a)===m)break}else for(e in a)if(l.call(a,e)&&c.call(d,a[e],e,a)===m)break};b.map=function(a,c,b){var e=[];if(a==null)return e;if(t&&a.map===t)return a.map(c,b);h(a,function(a,g,G){e[e.length]=c.call(b,a,g,G)});return e};b.reduce=b.foldl=b.inject=function(a,c,d,e){var k=d!==void 0;a==null&&(a=[]);if(u&&a.reduce===u)return e&&(c=b.bind(c,e)),k?a.reduce(c,d):a.reduce(c);h(a,function(a,b,f){!k&&b===0?(d=a,k=!0):d=c.call(e,d,a,b,f)});if(!k)throw new TypeError("Reduce of empty array with no initial value");
10   -return d};b.reduceRight=b.foldr=function(a,c,d,e){a==null&&(a=[]);if(v&&a.reduceRight===v)return e&&(c=b.bind(c,e)),d!==void 0?a.reduceRight(c,d):a.reduceRight(c);a=(b.isArray(a)?a.slice():b.toArray(a)).reverse();return b.reduce(a,c,d,e)};b.find=b.detect=function(a,c,b){var e;A(a,function(a,g,f){if(c.call(b,a,g,f))return e=a,!0});return e};b.filter=b.select=function(a,c,b){var e=[];if(a==null)return e;if(w&&a.filter===w)return a.filter(c,b);h(a,function(a,g,f){c.call(b,a,g,f)&&(e[e.length]=a)});return e};
11   -b.reject=function(a,c,b){var e=[];if(a==null)return e;h(a,function(a,g,f){c.call(b,a,g,f)||(e[e.length]=a)});return e};b.every=b.all=function(a,c,b){var e=!0;if(a==null)return e;if(x&&a.every===x)return a.every(c,b);h(a,function(a,g,f){if(!(e=e&&c.call(b,a,g,f)))return m});return e};var A=b.some=b.any=function(a,c,d){c||(c=b.identity);var e=!1;if(a==null)return e;if(y&&a.some===y)return a.some(c,d);h(a,function(a,b,f){if(e=c.call(d,a,b,f))return m});return e};b.include=b.contains=function(a,c){var b=
12   -!1;if(a==null)return b;if(o&&a.indexOf===o)return a.indexOf(c)!=-1;A(a,function(a){if(b=a===c)return!0});return b};b.invoke=function(a,c){var d=f.call(arguments,2);return b.map(a,function(a){return(c.call?c||a:a[c]).apply(a,d)})};b.pluck=function(a,c){return b.map(a,function(a){return a[c]})};b.max=function(a,c,d){if(!c&&b.isArray(a))return Math.max.apply(Math,a);var e={computed:-Infinity};h(a,function(a,b,f){b=c?c.call(d,a,b,f):a;b>=e.computed&&(e={value:a,computed:b})});return e.value};b.min=function(a,
13   -c,d){if(!c&&b.isArray(a))return Math.min.apply(Math,a);var e={computed:Infinity};h(a,function(a,b,f){b=c?c.call(d,a,b,f):a;b<e.computed&&(e={value:a,computed:b})});return e.value};b.sortBy=function(a,c,d){return b.pluck(b.map(a,function(a,b,f){return{value:a,criteria:c.call(d,a,b,f)}}).sort(function(a,b){var c=a.criteria,d=b.criteria;return c<d?-1:c>d?1:0}),"value")};b.sortedIndex=function(a,c,d){d||(d=b.identity);for(var e=0,f=a.length;e<f;){var g=e+f>>1;d(a[g])<d(c)?e=g+1:f=g}return e};b.toArray=
14   -function(a){if(!a)return[];if(a.toArray)return a.toArray();if(b.isArray(a))return a;if(b.isArguments(a))return f.call(a);return b.values(a)};b.size=function(a){return b.toArray(a).length};b.first=b.head=function(a,b,d){return b!=null&&!d?f.call(a,0,b):a[0]};b.rest=b.tail=function(a,b,d){return f.call(a,b==null||d?1:b)};b.last=function(a){return a[a.length-1]};b.compact=function(a){return b.filter(a,function(a){return!!a})};b.flatten=function(a){return b.reduce(a,function(a,d){if(b.isArray(d))return a.concat(b.flatten(d));
15   -a[a.length]=d;return a},[])};b.without=function(a){var c=f.call(arguments,1);return b.filter(a,function(a){return!b.include(c,a)})};b.uniq=b.unique=function(a,c){return b.reduce(a,function(a,e,f){if(0==f||(c===!0?b.last(a)!=e:!b.include(a,e)))a[a.length]=e;return a},[])};b.intersect=function(a){var c=f.call(arguments,1);return b.filter(b.uniq(a),function(a){return b.every(c,function(c){return b.indexOf(c,a)>=0})})};b.zip=function(){for(var a=f.call(arguments),c=b.max(b.pluck(a,"length")),d=Array(c),
16   -e=0;e<c;e++)d[e]=b.pluck(a,""+e);return d};b.indexOf=function(a,c,d){if(a==null)return-1;var e;if(d)return d=b.sortedIndex(a,c),a[d]===c?d:-1;if(o&&a.indexOf===o)return a.indexOf(c);d=0;for(e=a.length;d<e;d++)if(a[d]===c)return d;return-1};b.lastIndexOf=function(a,b){if(a==null)return-1;if(z&&a.lastIndexOf===z)return a.lastIndexOf(b);for(var d=a.length;d--;)if(a[d]===b)return d;return-1};b.range=function(a,b,d){arguments.length<=1&&(b=a||0,a=0);d=arguments[2]||1;for(var e=Math.max(Math.ceil((b-a)/
17   -d),0),f=0,g=Array(e);f<e;)g[f++]=a,a+=d;return g};b.bind=function(a,b){if(a.bind===q&&q)return q.apply(a,f.call(arguments,1));var d=f.call(arguments,2);return function(){return a.apply(b,d.concat(f.call(arguments)))}};b.bindAll=function(a){var c=f.call(arguments,1);c.length==0&&(c=b.functions(a));h(c,function(c){a[c]=b.bind(a[c],a)});return a};b.memoize=function(a,c){var d={};c||(c=b.identity);return function(){var b=c.apply(this,arguments);return l.call(d,b)?d[b]:d[b]=a.apply(this,arguments)}};b.delay=
18   -function(a,b){var d=f.call(arguments,2);return setTimeout(function(){return a.apply(a,d)},b)};b.defer=function(a){return b.delay.apply(b,[a,1].concat(f.call(arguments,1)))};var B=function(a,b,d){var e;return function(){var f=this,g=arguments,h=function(){e=null;a.apply(f,g)};d&&clearTimeout(e);if(d||!e)e=setTimeout(h,b)}};b.throttle=function(a,b){return B(a,b,!1)};b.debounce=function(a,b){return B(a,b,!0)};b.once=function(a){var b=!1,d;return function(){if(b)return d;b=!0;return d=a.apply(this,arguments)}};
19   -b.wrap=function(a,b){return function(){var d=[a].concat(f.call(arguments));return b.apply(this,d)}};b.compose=function(){var a=f.call(arguments);return function(){for(var b=f.call(arguments),d=a.length-1;d>=0;d--)b=[a[d].apply(this,b)];return b[0]}};b.after=function(a,b){return function(){if(--a<1)return b.apply(this,arguments)}};b.keys=F||function(a){if(a!==Object(a))throw new TypeError("Invalid object");var b=[],d;for(d in a)l.call(a,d)&&(b[b.length]=d);return b};b.values=function(a){return b.map(a,
20   -b.identity)};b.functions=b.methods=function(a){return b.filter(b.keys(a),function(c){return b.isFunction(a[c])}).sort()};b.extend=function(a){h(f.call(arguments,1),function(b){for(var d in b)b[d]!==void 0&&(a[d]=b[d])});return a};b.defaults=function(a){h(f.call(arguments,1),function(b){for(var d in b)a[d]==null&&(a[d]=b[d])});return a};b.clone=function(a){return b.isArray(a)?a.slice():b.extend({},a)};b.tap=function(a,b){b(a);return a};b.isEqual=function(a,c){if(a===c)return!0;var d=typeof a;if(d!=
21   -typeof c)return!1;if(a==c)return!0;if(!a&&c||a&&!c)return!1;if(a._chain)a=a._wrapped;if(c._chain)c=c._wrapped;if(a.isEqual)return a.isEqual(c);if(b.isDate(a)&&b.isDate(c))return a.getTime()===c.getTime();if(b.isNaN(a)&&b.isNaN(c))return!1;if(b.isRegExp(a)&&b.isRegExp(c))return a.source===c.source&&a.global===c.global&&a.ignoreCase===c.ignoreCase&&a.multiline===c.multiline;if(d!=="object")return!1;if(a.length&&a.length!==c.length)return!1;d=b.keys(a);var e=b.keys(c);if(d.length!=e.length)return!1;
22   -for(var f in a)if(!(f in c)||!b.isEqual(a[f],c[f]))return!1;return!0};b.isEmpty=function(a){if(b.isArray(a)||b.isString(a))return a.length===0;for(var c in a)if(l.call(a,c))return!1;return!0};b.isElement=function(a){return!!(a&&a.nodeType==1)};b.isArray=n||function(a){return E.call(a)==="[object Array]"};b.isArguments=function(a){return!(!a||!l.call(a,"callee"))};b.isFunction=function(a){return!(!a||!a.constructor||!a.call||!a.apply)};b.isString=function(a){return!!(a===""||a&&a.charCodeAt&&a.substr)};
23   -b.isNumber=function(a){return!!(a===0||a&&a.toExponential&&a.toFixed)};b.isNaN=function(a){return a!==a};b.isBoolean=function(a){return a===!0||a===!1};b.isDate=function(a){return!(!a||!a.getTimezoneOffset||!a.setUTCFullYear)};b.isRegExp=function(a){return!(!a||!a.test||!a.exec||!(a.ignoreCase||a.ignoreCase===!1))};b.isNull=function(a){return a===null};b.isUndefined=function(a){return a===void 0};b.noConflict=function(){p._=C;return this};b.identity=function(a){return a};b.times=function(a,b,d){for(var e=
24   -0;e<a;e++)b.call(d,e)};b.mixin=function(a){h(b.functions(a),function(c){H(c,b[c]=a[c])})};var I=0;b.uniqueId=function(a){var b=I++;return a?a+b:b};b.templateSettings={evaluate:/<%([\s\S]+?)%>/g,interpolate:/<%=([\s\S]+?)%>/g};b.template=function(a,c){var d=b.templateSettings;d="var __p=[],print=function(){__p.push.apply(__p,arguments);};with(obj||{}){__p.push('"+a.replace(/\\/g,"\\\\").replace(/'/g,"\\'").replace(d.interpolate,function(a,b){return"',"+b.replace(/\\'/g,"'")+",'"}).replace(d.evaluate||
25   -null,function(a,b){return"');"+b.replace(/\\'/g,"'").replace(/[\r\n\t]/g," ")+"__p.push('"}).replace(/\r/g,"\\r").replace(/\n/g,"\\n").replace(/\t/g,"\\t")+"');}return __p.join('');";d=new Function("obj",d);return c?d(c):d};var j=function(a){this._wrapped=a};b.prototype=j.prototype;var r=function(a,c){return c?b(a).chain():a},H=function(a,c){j.prototype[a]=function(){var a=f.call(arguments);D.call(a,this._wrapped);return r(c.apply(b,a),this._chain)}};b.mixin(b);h(["pop","push","reverse","shift","sort",
26   -"splice","unshift"],function(a){var b=i[a];j.prototype[a]=function(){b.apply(this._wrapped,arguments);return r(this._wrapped,this._chain)}});h(["concat","join","slice"],function(a){var b=i[a];j.prototype[a]=function(){return r(b.apply(this._wrapped,arguments),this._chain)}});j.prototype.chain=function(){this._chain=!0;return this};j.prototype.value=function(){return this._wrapped}})();
27 0 \ No newline at end of file
app/assets/stylesheets/errbit.css
... ... @@ -586,8 +586,8 @@ div.issue_tracker.nested label.r_on, div.issue_tracker.nested label.r_on:hover,
586 586 table.apps tbody tr:hover td ,table.errs tbody tr:hover td { background-color: #F2F2F2;}
587 587  
588 588 table.apps td.name, table.errs td.message { width: 100%; }
589   -table.apps td { padding: 16px 25px; }
590   -table.apps th { padding: 10px 25px; }
  589 +table.apps td { padding: 16px 20px; }
  590 +table.apps th { padding: 10px 20px; }
591 591  
592 592 table.apps td.issue_tracker, table.apps td.count, table.apps td.deploy {
593 593 text-align: center;
... ... @@ -894,6 +894,11 @@ table.errs tr td.message .inline_comment em.commenter {
894 894 color: #777;
895 895 }
896 896  
  897 +textarea#comment_body {
  898 + width: 420px;
  899 + height: 80px;
  900 +}
  901 +
897 902 .current.asc:after { content: ' ↑'; }
898 903 .current.desc:after { content: ' ↓'; }
899 904  
... ...
app/assets/stylesheets/images/button-bg.png

148 Bytes | W: | H:

108 Bytes | W: | H:

  • 2-up
  • Swipe
  • Onion skin
app/assets/stylesheets/images/content-fade.png

174 Bytes | W: | H:

134 Bytes | W: | H:

  • 2-up
  • Swipe
  • Onion skin
app/assets/stylesheets/images/error-badge-bg.png

119 Bytes | W: | H:

75 Bytes | W: | H:

  • 2-up
  • Swipe
  • Onion skin
app/assets/stylesheets/images/header.png

196 Bytes | W: | H:

109 Bytes | W: | H:

  • 2-up
  • Swipe
  • Onion skin
app/assets/stylesheets/images/icons/add.png

1.04 KB | W: | H:

908 Bytes | W: | H:

  • 2-up
  • Swipe
  • Onion skin
app/assets/stylesheets/images/icons/briefcase.png

675 Bytes | W: | H:

635 Bytes | W: | H:

  • 2-up
  • Swipe
  • Onion skin
app/assets/stylesheets/images/icons/bullet-red-sm.png

417 Bytes | W: | H:

338 Bytes | W: | H:

  • 2-up
  • Swipe
  • Onion skin
app/assets/stylesheets/images/icons/cross.png

473 Bytes | W: | H:

285 Bytes | W: | H:

  • 2-up
  • Swipe
  • Onion skin
app/assets/stylesheets/images/icons/edit.png

1.22 KB | W: | H:

1.13 KB | W: | H:

  • 2-up
  • Swipe
  • Onion skin
app/assets/stylesheets/images/icons/error.png

1.05 KB | W: | H:

993 Bytes | W: | H:

  • 2-up
  • Swipe
  • Onion skin
app/assets/stylesheets/images/icons/github.png

1.94 KB | W: | H:

1.57 KB | W: | H:

  • 2-up
  • Swipe
  • Onion skin
app/assets/stylesheets/images/icons/ical.png

1.86 KB | W: | H:

1.59 KB | W: | H:

  • 2-up
  • Swipe
  • Onion skin
app/assets/stylesheets/images/icons/notice.png

157 Bytes | W: | H:

112 Bytes | W: | H:

  • 2-up
  • Swipe
  • Onion skin
app/assets/stylesheets/images/icons/required.png

250 Bytes | W: | H:

198 Bytes | W: | H:

  • 2-up
  • Swipe
  • Onion skin
app/assets/stylesheets/images/icons/right-arrow.png

1.11 KB | W: | H:

847 Bytes | W: | H:

  • 2-up
  • Swipe
  • Onion skin
app/assets/stylesheets/images/icons/success.png

1.06 KB | W: | H:

912 Bytes | W: | H:

  • 2-up
  • Swipe
  • Onion skin
app/assets/stylesheets/images/icons/thumbs-up.png

1.42 KB | W: | H:

1.28 KB | W: | H:

  • 2-up
  • Swipe
  • Onion skin
app/assets/stylesheets/images/icons/trash.png

1.65 KB | W: | H:

1.59 KB | W: | H:

  • 2-up
  • Swipe
  • Onion skin
app/assets/stylesheets/images/icons/unlink_github.png

2.02 KB | W: | H:

1.87 KB | W: | H:

  • 2-up
  • Swipe
  • Onion skin
app/assets/stylesheets/images/icons/up.png

1.11 KB | W: | H:

837 Bytes | W: | H:

  • 2-up
  • Swipe
  • Onion skin
app/assets/stylesheets/images/icons/user.png

877 Bytes | W: | H:

822 Bytes | W: | H:

  • 2-up
  • Swipe
  • Onion skin
app/assets/stylesheets/images/icons/warning.png

674 Bytes | W: | H:

569 Bytes | W: | H:

  • 2-up
  • Swipe
  • Onion skin
app/assets/stylesheets/images/logo.png

3.21 KB | W: | H:

2.96 KB | W: | H:

  • 2-up
  • Swipe
  • Onion skin
app/assets/stylesheets/images/notebook.png

133 Bytes | W: | H:

91 Bytes | W: | H:

  • 2-up
  • Swipe
  • Onion skin
app/assets/stylesheets/images/resolved-badge-bg.png

119 Bytes | W: | H:

75 Bytes | W: | H:

  • 2-up
  • Swipe
  • Onion skin
app/assets/stylesheets/issue_tracker_icons.css.erb
... ... @@ -3,16 +3,16 @@
3 3  
4 4 <% trackers.each do |tracker| %>
5 5 div.issue_tracker.nested label.<%= tracker %> {
6   - background: url(/assets/<%= tracker %>_inactive.png) no-repeat;
  6 + background: url(<%= tracker %>_inactive.png) no-repeat;
7 7 }
8 8 div.issue_tracker.nested label.r_on.<%= tracker %> {
9   - background: url(/assets/<%= tracker %>_create.png) no-repeat;
  9 + background: url(<%= tracker %>_create.png) no-repeat;
10 10 }
11 11 #action-bar a.<%= tracker %>_create {
12   - background: transparent url(/assets/<%= tracker %>_create.png) 6px 5px no-repeat;
  12 + background: transparent url(<%= tracker %>_create.png) 6px 5px no-repeat;
13 13 }
14 14 #action-bar a.<%= tracker %>_goto {
15   - background: transparent url(/assets/<%= tracker %>_goto.png) 6px 5px no-repeat;
  15 + background: transparent url(<%= tracker %>_goto.png) 6px 5px no-repeat;
16 16 }
17 17 <% end %>
18 18  
... ...
app/assets/stylesheets/jquery.alerts.css
... ... @@ -17,7 +17,7 @@
17 17 text-align: center;
18 18 line-height: 1.75em;
19 19 color: #666;
20   - background: #CCC url(/images/alerts/title.gif) top repeat-x;
  20 + background: #CCC url(alerts/title.gif) top repeat-x;
21 21 border: solid 1px #FFF;
22 22 border-bottom: solid 1px #999;
23 23 cursor: default;
... ... @@ -26,21 +26,21 @@
26 26 }
27 27  
28 28 #popup_content {
29   - background: 16px 16px no-repeat url(/images/alerts/info.gif);
  29 + background: 16px 16px no-repeat url(alerts/info.gif);
30 30 padding: 1em 1.75em;
31 31 margin: 0em;
32 32 }
33 33  
34 34 #popup_content.alert {
35   - background-image: url(/images/alerts/info.gif);
  35 + background-image: url(alerts/info.gif);
36 36 }
37 37  
38 38 #popup_content.confirm {
39   - background-image: url(/images/alerts/important.gif);
  39 + background-image: url(alerts/important.gif);
40 40 }
41 41  
42 42 #popup_content.prompt {
43   - background-image: url(/images/alerts/help.gif);
  43 + background-image: url(alerts/help.gif);
44 44 }
45 45  
46 46 #popup_message {
... ...
app/assets/stylesheets/notification_service_icons.css.erb
... ... @@ -3,16 +3,16 @@
3 3  
4 4 <% notification_services.each do |notification_service| %>
5 5 div.notification_service.nested label.<%= notification_service %> {
6   - background: url(/assets/<%= notification_service %>_inactive.png) no-repeat;
  6 + background: url(<%= notification_service %>_inactive.png) no-repeat;
7 7 }
8 8 div.notification_service.nested label.r_on.<%= notification_service %> {
9   - background: url(/assets/<%= notification_service %>_create.png) no-repeat;
  9 + background: url(<%= notification_service %>_create.png) no-repeat;
10 10 }
11 11 #action-bar a.<%= notification_service %>_create {
12   - background: transparent url(/assets/<%= notification_service %>_create.png) 6px 5px no-repeat;
  12 + background: transparent url(<%= notification_service %>_create.png) 6px 5px no-repeat;
13 13 }
14 14 #action-bar a.<%= notification_service %>_goto {
15   - background: transparent url(/assets/<%= notification_service %>_goto.png) 6px 5px no-repeat;
  15 + background: transparent url(<%= notification_service %>_goto.png) 6px 5px no-repeat;
16 16 }
17 17 <% end %>
18 18  
... ...
app/controllers/apps_controller.rb
... ... @@ -52,23 +52,7 @@ class AppsController &lt; InheritedResources::Base
52 52  
53 53 protected
54 54 def collection
55   - @unresolved_counts, @problem_counts = {}, {}
56   - @apps ||= begin
57   - apps = end_of_association_chain.all
58   -
59   - # Cache counts for unresolved errs and problems
60   - apps.each do |app|
61   - @unresolved_counts[app.id] ||= app.problems.unresolved.count
62   - @problem_counts[app.id] ||= app.problems.count
63   - end
64   -
65   - # Sort apps by number of unresolved errs, then problem counts.
66   - apps.sort do |a,b|
67   - (@unresolved_counts[b.id] <=> @unresolved_counts[a.id]).nonzero? ||
68   - (@problem_counts[b.id] <=> @problem_counts[a.id]).nonzero? ||
69   - a.name <=> b.name
70   - end
71   - end
  55 + @apps ||= end_of_association_chain.all.sort
72 56 end
73 57  
74 58 def initialize_subclassed_issue_tracker
... ... @@ -80,7 +64,7 @@ class AppsController &lt; InheritedResources::Base
80 64 end
81 65 end
82 66  
83   - def initialize_subclassed_notification_service
  67 + def initialize_subclassed_notification_service
84 68 # set the app's notification service
85 69 if params[:app][:notification_service_attributes] && notification_type = params[:app][:notification_service_attributes][:type]
86 70 if NotificationService.subclasses.map(&:name).concat(["NotificationService"]).include?(notification_type)
... ...
app/controllers/problems_controller.rb
1 1 class ProblemsController < ApplicationController
2   - include ActionView::Helpers::TextHelper
3   -
4 2 before_filter :find_app, :except => [:index, :all, :destroy_several, :resolve_several, :unresolve_several, :merge_several, :unmerge_several]
5 3 before_filter :find_problem, :except => [:index, :all, :destroy_several, :resolve_several, :unresolve_several, :merge_several, :unmerge_several]
6 4 before_filter :find_selected_problems, :only => [:destroy_several, :resolve_several, :unresolve_several, :merge_several, :unmerge_several]
... ... @@ -37,36 +35,10 @@ class ProblemsController &lt; ApplicationController
37 35 end
38 36  
39 37 def create_issue
40   - # Create an issue on GitHub using user's github token
41   - if params[:tracker] == 'user_github'
42   - if !@app.github_repo?
43   - flash[:error] = "This app doesn't have a GitHub repo set up."
44   - elsif !current_user.github_account?
45   - flash[:error] = "You haven't linked your Github account."
46   - else
47   - @tracker = GithubIssuesTracker.new(
48   - :app => @app,
49   - :username => current_user.github_login,
50   - :oauth_token => current_user.github_oauth_token
51   - )
52   - end
53   -
54   - # Or, create an issue using the App's issue tracker
55   - elsif @app.issue_tracker_configured?
56   - @tracker = @app.issue_tracker
  38 + issue_creation = IssueCreation.new(@problem, current_user, params[:tracker])
57 39  
58   - # Otherwise, display error about missing tracker configuration.
59   - else
60   - flash[:error] = "This app has no issue tracker setup."
61   - end
62   -
63   - if flash[:error].blank? && @tracker
64   - begin
65   - @tracker.create_issue @problem, current_user
66   - rescue Exception => ex
67   - Rails.logger.error "Error during issue creation: " << ex.message
68   - flash[:error] = "There was an error during issue creation: #{ex.message}"
69   - end
  40 + unless issue_creation.execute
  41 + flash[:error] = issue_creation.errors[:base].first
70 42 end
71 43  
72 44 redirect_to app_problem_path(@app, @problem)
... ... @@ -87,13 +59,13 @@ class ProblemsController &lt; ApplicationController
87 59  
88 60 def resolve_several
89 61 @selected_problems.each(&:resolve!)
90   - flash[:success] = "Great news everyone! #{pluralize(@selected_problems.count, 'err has', 'errs have')} been resolved."
  62 + flash[:success] = "Great news everyone! #{I18n.t(:n_errs_have, :count => @selected_problems.count)} been resolved."
91 63 redirect_to :back
92 64 end
93 65  
94 66 def unresolve_several
95 67 @selected_problems.each(&:unresolve!)
96   - flash[:success] = "#{pluralize(@selected_problems.count, 'err has', 'errs have')} been unresolved."
  68 + flash[:success] = "#{I18n.t(:n_errs_have, :count => @selected_problems.count)} been unresolved."
97 69 redirect_to :back
98 70 end
99 71  
... ... @@ -109,13 +81,13 @@ class ProblemsController &lt; ApplicationController
109 81  
110 82 def unmerge_several
111 83 all = @selected_problems.map(&:unmerge!).flatten
112   - flash[:success] = "#{pluralize(all.length, 'err has', 'errs have')} been unmerged."
  84 + flash[:success] = "#{I18n.t(:n_errs_have, :count => all.length)} been unmerged."
113 85 redirect_to :back
114 86 end
115 87  
116 88 def destroy_several
117 89 nb_problem_destroy = ProblemDestroy.execute(@selected_problems)
118   - flash[:notice] = "#{pluralize(nb_problem_destroy, 'err has', 'errs have')} been deleted."
  90 + flash[:notice] = "#{I18n.t(:n_errs_have, :count => nb_problem_destroy)} been deleted."
119 91 redirect_to :back
120 92 end
121 93  
... ...
app/helpers/backtrace_line_helper.rb
... ... @@ -19,13 +19,13 @@ module BacktraceLineHelper
19 19  
20 20 def link_to_github(line, text = nil)
21 21 return unless line.app.github_repo?
22   - href = "%s#L%s" % [line.app.github_url_to_file(line.file), line.number]
  22 + href = "%s#L%s" % [line.app.github_url_to_file(line.decorated_path + line.file_name), line.number]
23 23 link_to(text || line.file_name, href, :target => '_blank')
24 24 end
25 25  
26 26 def link_to_bitbucket(line, text = nil)
27 27 return unless line.app.bitbucket_repo?
28   - href = "%s#cl-%s" % [line.app.bitbucket_url_to_file(line.file), line.number]
  28 + href = "%s#cl-%s" % [line.app.bitbucket_url_to_file(line.decorated_path + line.file_name), line.number]
29 29 link_to(text || line.file_name, href, :target => '_blank')
30 30 end
31 31  
... ...
app/helpers/notices_helper.rb
1 1 # encoding: utf-8
2 2 module NoticesHelper
3 3 def notice_atom_summary(notice)
4   - render "notices/atom_entry.html.haml", :notice => notice
  4 + render "notices/atom_entry", :notice => notice
5 5 end
6 6 end
7   -
... ...
app/interactors/issue_creation.rb 0 → 100644
... ... @@ -0,0 +1,55 @@
  1 +class IssueCreation
  2 + include ActiveModel::Validations
  3 +
  4 + attr_reader :problem, :user, :tracker_name
  5 +
  6 + delegate :app, :to => :problem
  7 +
  8 + def initialize(problem, user, tracker_name)
  9 + @problem = problem
  10 + @user = user
  11 + @tracker_name = tracker_name
  12 + end
  13 +
  14 + def tracker
  15 + return @tracker if @tracker
  16 +
  17 + # Create an issue on GitHub using user's github token
  18 + if tracker_name == 'user_github'
  19 + if !app.github_repo?
  20 + errors.add :base, "This app doesn't have a GitHub repo set up."
  21 + elsif !user.github_account?
  22 + errors.add :base, "You haven't linked your Github account."
  23 + else
  24 + @tracker = GithubIssuesTracker.new(
  25 + :app => app,
  26 + :username => user.github_login,
  27 + :oauth_token => user.github_oauth_token
  28 + )
  29 + end
  30 +
  31 + # Or, create an issue using the App's issue tracker
  32 + elsif app.issue_tracker_configured?
  33 + @tracker = app.issue_tracker
  34 +
  35 + # Otherwise, display error about missing tracker configuration.
  36 + else
  37 + errors.add :base, "This app has no issue tracker setup."
  38 + end
  39 +
  40 + @tracker
  41 + end
  42 +
  43 + def execute
  44 + if tracker
  45 + begin
  46 + tracker.create_issue problem, user
  47 + rescue => ex
  48 + Rails.logger.error "Error during issue creation: " << ex.message
  49 + errors.add :base, "There was an error during issue creation: #{ex.message}"
  50 + end
  51 + end
  52 +
  53 + errors.empty?
  54 + end
  55 +end
... ...
app/models/app.rb
1 1 class App
2 2 include Mongoid::Document
3 3 include Mongoid::Timestamps
  4 + include Comparable
4 5  
5 6 field :name, :type => String
6 7 field :api_key
... ... @@ -98,12 +99,16 @@ class App
98 99  
99 100 # Legacy apps don't have notify_on_errs and notify_on_deploys params
100 101 def notify_on_errs
101   - !(self[:notify_on_errs] == false)
  102 + !(super == false)
102 103 end
103 104 alias :notify_on_errs? :notify_on_errs
104 105  
  106 + def notifiable?
  107 + notify_on_errs? && notification_recipients.any?
  108 + end
  109 +
105 110 def notify_on_deploys
106   - !(self[:notify_on_deploys] == false)
  111 + !(super == false)
107 112 end
108 113 alias :notify_on_deploys? :notify_on_deploys
109 114  
... ... @@ -137,11 +142,11 @@ class App
137 142  
138 143  
139 144 def issue_tracker_configured?
140   - !!(issue_tracker && issue_tracker.class < IssueTracker && issue_tracker.project_id.present?)
  145 + !!(issue_tracker.class < IssueTracker && issue_tracker.configured?)
141 146 end
142 147  
143 148 def notification_service_configured?
144   - !!(notification_service && notification_service.class < NotificationService && notification_service.api_token.present?)
  149 + !!(notification_service.class < NotificationService && notification_service.configured?)
145 150 end
146 151  
147 152  
... ... @@ -169,6 +174,25 @@ class App
169 174 end
170 175 end
171 176  
  177 + def unresolved_count
  178 + @unresolved_count ||= problems.unresolved.count
  179 + end
  180 +
  181 + def problem_count
  182 + @problem_count ||= problems.count
  183 + end
  184 +
  185 + # Compare by number of unresolved errs, then problem counts.
  186 + def <=>(other)
  187 + (other.unresolved_count <=> unresolved_count).nonzero? ||
  188 + (other.problem_count <=> problem_count).nonzero? ||
  189 + name <=> other.name
  190 + end
  191 +
  192 + def email_at_notices
  193 + Errbit::Config.per_app_email_at_notices ? super : Errbit::Config.email_at_notices
  194 + end
  195 +
172 196 protected
173 197  
174 198 def store_cached_attributes_on_problems
... ...
app/models/issue_tracker.rb
... ... @@ -35,5 +35,9 @@ class IssueTracker
35 35 Label = ''
36 36 def self.label; self::Label; end
37 37 def label; self.class.label; end
  38 +
  39 + def configured?
  40 + project_id.present?
  41 + end
38 42 end
39 43  
... ...
app/models/issue_trackers/github_issues_tracker.rb
... ... @@ -35,7 +35,7 @@ if defined? Octokit
35 35 end
36 36  
37 37 begin
38   - issue = client.create_issue(project_id, issue_title(problem), body_template.result(binding).unpack('C*').pack('U*'), options = {})
  38 + issue = client.create_issue(project_id, issue_title(problem), body_template.result(binding).unpack('C*').pack('U*'))
39 39 problem.update_attributes(
40 40 :issue_link => issue.html_url,
41 41 :issue_type => Label
... ... @@ -54,4 +54,4 @@ if defined? Octokit
54 54 "https://github.com/#{project_id}/issues"
55 55 end
56 56 end
57   -end
58 57 \ No newline at end of file
  58 +end
... ...
app/models/issue_trackers/gitlab_tracker.rb 0 → 100644
... ... @@ -0,0 +1,43 @@
  1 +if defined? Gitlab
  2 + class IssueTrackers::GitlabTracker < IssueTracker
  3 + Label = "gitlab"
  4 + Fields = [
  5 + [:account, {
  6 + :label => "Gitlab URL",
  7 + :placeholder => "e.g. https://example.net"
  8 + }],
  9 + [:api_token, {
  10 + :placeholder => "API Token for your account"
  11 + }],
  12 + [:project_id, {
  13 + :label => "Ticket Project Short Name / ID",
  14 + :placeholder => "Gitlab Project where issues will be created"
  15 + }]
  16 + ]
  17 +
  18 + def check_params
  19 + if Fields.detect {|f| self[f[0]].blank?}
  20 + errors.add :base, 'You must specify your Gitlab URL, API token and Project ID'
  21 + end
  22 + end
  23 +
  24 + def create_issue(problem, reported_by = nil)
  25 + Gitlab.configure do |config|
  26 + config.endpoint = "#{account}/api/v2"
  27 + config.private_token = api_token
  28 + config.user_agent = 'Errbit User Agent'
  29 + end
  30 + title = issue_title problem
  31 + description = body_template.result(binding)
  32 + Gitlab.create_issue(project_id, title, { :description => description, :labels => "errbit" } )
  33 + end
  34 +
  35 + def body_template
  36 + @@body_template ||= ERB.new(File.read(Rails.root + "app/views/issue_trackers/gitlab_body.txt.erb").gsub(/^\s*/, ''))
  37 + end
  38 +
  39 + def url
  40 + "#{account}/#{project_id}/issues"
  41 + end
  42 + end
  43 +end
... ...
app/models/issue_trackers/lighthouse_tracker.rb
... ... @@ -3,19 +3,22 @@ if defined? Lighthouse
3 3 Label = "lighthouseapp"
4 4 Fields = [
5 5 [:account, {
6   - :placeholder => "abc from http://abc.lighthouseapp.com"
  6 + :label => "Subdomain",
  7 + :placeholder => "subdomain from http://{{subdomain}}.lighthouseapp.com"
7 8 }],
8 9 [:api_token, {
9   - :placeholder => "API Token for your account"
  10 + :label => "API Token",
  11 + :placeholder => "123456789abcdef123456789abcdef"
10 12 }],
11 13 [:project_id, {
12   - :placeholder => "Lighthouse project"
  14 + :label => "Project ID",
  15 + :placeholder => "123456"
13 16 }]
14 17 ]
15 18  
16 19 def check_params
17 20 if Fields.detect {|f| self[f[0]].blank? }
18   - errors.add :base, 'You must specify your Lighthouseapp account, API token and Project ID'
  21 + errors.add :base, 'You must specify your Lighthouseapp Subdomain, API token and Project ID'
19 22 end
20 23 end
21 24  
... ...
app/models/issue_trackers/redmine_tracker.rb
... ... @@ -4,7 +4,7 @@ if defined? RedmineClient
4 4 Fields = [
5 5 [:account, {
6 6 :label => "Redmine URL",
7   - :placeholder => "e.g. http://www.redmine.org/"
  7 + :placeholder => "http://www.redmine.org/"
8 8 }],
9 9 [:api_token, {
10 10 :placeholder => "API Token for your account"
... ...
app/models/notice.rb
... ... @@ -13,7 +13,7 @@ class Notice
13 13 field :current_user, :type => Hash
14 14 field :error_class
15 15 delegate :lines, :to => :backtrace, :prefix => true
16   - delegate :app, :to => :err
  16 + delegate :app, :problem, :to => :err
17 17  
18 18 belongs_to :err
19 19 belongs_to :backtrace, :index => true
... ... @@ -36,8 +36,6 @@ class Notice
36 36 scope :reverse_ordered, order_by(:created_at.desc)
37 37 scope :for_errs, lambda {|errs| where(:err_id.in => errs.all.map(&:id))}
38 38  
39   - delegate :app, :problem, :to => :err
40   -
41 39 def user_agent
42 40 agent_string = env_vars['HTTP_USER_AGENT']
43 41 agent_string.blank? ? nil : UserAgent.parse(agent_string)
... ... @@ -66,7 +64,7 @@ class Notice
66 64 end
67 65  
68 66 def request
69   - read_attribute(:request) || {}
  67 + super || {}
70 68 end
71 69  
72 70 def url
... ... @@ -96,6 +94,18 @@ class Notice
96 94 backtrace_lines.in_app
97 95 end
98 96  
  97 + def similar_count
  98 + problem.notices_count
  99 + end
  100 +
  101 + def notifiable?
  102 + app.email_at_notices.include?(similar_count)
  103 + end
  104 +
  105 + def should_notify?
  106 + app.notifiable? && notifiable?
  107 + end
  108 +
99 109 protected
100 110  
101 111 def increase_counter_cache
... ...
app/models/notice_observer.rb
... ... @@ -7,16 +7,7 @@ class NoticeObserver &lt; Mongoid::Observer
7 7 notice.app.notification_service.create_notification(notice.problem)
8 8 end
9 9  
10   - if notice.app.notification_recipients.any?
11   - Mailer.err_notification(notice).deliver
12   - end
  10 + Mailer.err_notification(notice).deliver if notice.should_notify?
13 11 end
14 12  
15   - private
16   -
17   - def should_notify? notice
18   - app = notice.app
19   - app.notify_on_errs? and (app.notification_recipients.any? or !app.notification_service.nil?) and
20   - (app.email_at_notices or Errbit::Config.email_at_notices).include?(notice.problem.notices_count)
21   - end
22 13 end
... ...
app/models/notification_service.rb
... ... @@ -30,4 +30,8 @@ class NotificationService
30 30 Label = ''
31 31 def self.label; self::Label; end
32 32 def label; self.class.label; end
  33 +
  34 + def configured?
  35 + api_token.present?
  36 + end
33 37 end
... ...
app/models/notification_services/campfire_service.rb
... ... @@ -2,16 +2,18 @@ if defined? Campy
2 2 class NotificationServices::CampfireService < NotificationService
3 3 Label = "campfire"
4 4 Fields = [
5   - [:subdomain, {
6   - :placeholder => "Campfire Subdomain"
7   - }],
8   - [:api_token, {
9   - :placeholder => "API Token"
10   - }],
11   - [:room_id, {
12   - :placeholder => "Room ID",
13   - :label => "Room ID"
14   - }],
  5 + [:subdomain, {
  6 + :label => "Subdomain",
  7 + :placeholder => "subdomain from http://{{subdomain}}.campfirenow.com"
  8 + }],
  9 + [:api_token, {
  10 + :label => "API Token",
  11 + :placeholder => "123456789abcdef123456789abcdef"
  12 + }],
  13 + [:room_id, {
  14 + :label => "Room ID",
  15 + :placeholder => "123456"
  16 + }]
15 17 ]
16 18  
17 19 def check_params
... ... @@ -21,7 +23,7 @@ if defined? Campy
21 23 end
22 24  
23 25 def url
24   - "http://campfirenow.com/"
  26 + "http://#{subdomain}.campfirenow.com/"
25 27 end
26 28  
27 29 def create_notification(problem)
... ... @@ -31,4 +33,4 @@ if defined? Campy
31 33 campy.speak "[errbit] #{problem.app.name} #{notification_description problem} - http://#{Errbit::Config.host}/apps/#{problem.app.id.to_s}/problems/#{problem.id.to_s}"
32 34 end
33 35 end
34 36 -end
  37 +end
35 38 \ No newline at end of file
... ...
app/models/notification_services/hipchat_service.rb
... ... @@ -6,8 +6,8 @@ if defined? HipChat
6 6 :placeholder => "API Token"
7 7 }],
8 8 [:room_id, {
9   - :placeholder => "Room ID",
10   - :label => "Room ID"
  9 + :placeholder => "Room name",
  10 + :label => "Room name"
11 11 }],
12 12 ]
13 13  
... ... @@ -17,6 +17,10 @@ if defined? HipChat
17 17 end
18 18 end
19 19  
  20 + def url
  21 + "https://www.hipchat.com/sign_in"
  22 + end
  23 +
20 24 def create_notification(problem)
21 25 url = app_problem_url problem.app, problem
22 26 message = <<-MSG.strip_heredoc
... ... @@ -28,4 +32,4 @@ if defined? HipChat
28 32 client[room_id].send('Errbit', message, :color => 'red')
29 33 end
30 34 end
31   -end
32 35 \ No newline at end of file
  36 +end
... ...
app/models/notification_services/hoiio_service.rb
... ... @@ -21,6 +21,10 @@ class NotificationServices::HoiioService &lt; NotificationService
21 21 end
22 22 end
23 23  
  24 + def url
  25 + "https://secure.hoiio.com/user/"
  26 + end
  27 +
24 28 def notification_description(problem)
25 29 "[#{ problem.environment }]#{problem.message.to_s.truncate(50)}"
26 30 end
... ...
app/models/notification_services/pushover_service.rb
... ... @@ -17,6 +17,10 @@ class NotificationServices::PushoverService &lt; NotificationService
17 17 end
18 18 end
19 19  
  20 + def url
  21 + "https://pushover.net/login"
  22 + end
  23 +
20 24 def create_notification(problem)
21 25 # build the hoi client
22 26 notification = Rushover::Client.new(subdomain)
... ...
app/models/problem.rb
... ... @@ -57,6 +57,10 @@ class Problem
57 57 Notice.for_errs(errs).ordered
58 58 end
59 59  
  60 + def comments_allowed?
  61 + Errbit::Config.allow_comments_with_issue_tracker || !app.issue_tracker_configured?
  62 + end
  63 +
60 64 def resolve!
61 65 self.update_attributes!(:resolved => true, :resolved_at => Time.now)
62 66 end
... ...
app/models/user.rb
... ... @@ -33,7 +33,7 @@ class User
33 33 end
34 34  
35 35 def per_page
36   - self[:per_page] || PER_PAGE
  36 + super || PER_PAGE
37 37 end
38 38  
39 39 def watching?(app)
... ...
app/views/apps/index.html.haml
... ... @@ -47,8 +47,8 @@
47 47 - revision = app.deploys.last.short_revision
48 48 = link_to( app.last_deploy_at.to_s(:micro) << (revision.present? ? " (#{revision})" : ""), app_deploys_path(app))
49 49 %td.count
50   - - if @problem_counts[app.id] > 0
51   - - unresolved = @unresolved_counts[app.id]
  50 + - if app.problem_count > 0
  51 + - unresolved = app.unresolved_count
52 52 = link_to unresolved, app_path(app), :class => (unresolved == 0 ? "resolved" : nil)
53 53 - if @apps.none?
54 54 %tr
... ...
app/views/issue_trackers/gitlab_body.txt.erb 0 → 100644
... ... @@ -0,0 +1,45 @@
  1 +[See this exception on Errbit](<%= app_problem_url problem.app, problem %> "See this exception on Errbit")
  2 +<% if notice = problem.notices.first %>
  3 +# <%= notice.message %> #
  4 +## Summary ##
  5 +<% if notice.request['url'].present? %>
  6 + ### URL ###
  7 + [<%= notice.request['url'] %>](<%= notice.request['url'] %>)"
  8 +<% end %>
  9 +### Where ###
  10 +<%= notice.where %>
  11 +
  12 +### Occured ###
  13 +<%= notice.created_at.to_s(:micro) %>
  14 +
  15 +### Similar ###
  16 +<%= (notice.problem.notices_count - 1).to_s %>
  17 +
  18 +## Params ##
  19 +```
  20 +<%= pretty_hash(notice.params) %>
  21 +```
  22 +
  23 +## Session ##
  24 +```
  25 +<%= pretty_hash(notice.session) %>
  26 +```
  27 +
  28 +## Backtrace ##
  29 +```
  30 +<% notice.backtrace_lines.each do |line| %><%= line.number %>: <%= line.file_relative %> -> **<%= line.method %>**
  31 +<% end %>
  32 +```
  33 +
  34 +## Environment ##
  35 +
  36 +<table>
  37 +<% for key, val in notice.env_vars %>
  38 + <tr>
  39 + <td><%= key %>:</td>
  40 + <td><%= val %></td>
  41 + </tr>
  42 +<% end %>
  43 +</table>
  44 +<% end %>
  45 +
... ...
app/views/layouts/application.html.haml
... ... @@ -30,7 +30,5 @@
30 30 - if content_for?(:comments)
31 31 #content-comments
32 32 = yield :comments
33   - #footer= "Powered by #{link_to 'Errbit', 'http://github.com/errbit/errbit', :target => '_blank'}: the open source error catcher.".html_safe
  33 + #footer Powered by #{link_to 'Errbit', 'http://github.com/errbit/errbit', :target => '_blank'}: the open source error catcher.
34 34 = yield :scripts
35   -
36   -= yield :before_title
37 35 \ No newline at end of file
... ...
app/views/notices/_atom_entry.atom.haml 0 → 100644
... ... @@ -0,0 +1,41 @@
  1 +%h2= notice.message
  2 +%h3 Summary
  3 +- if notice.request['url'].present?
  4 + %p
  5 + %strong URL:
  6 + = link_to(notice.request['url'], notice.request['url'])
  7 +%p
  8 + %strong Where:
  9 + = notice.where
  10 +%p
  11 + %strong Occured:
  12 + = notice.created_at.to_s(:micro)
  13 +%p
  14 + %strong Similar:
  15 + = notice.problem.notices_count - 1
  16 +
  17 +%h3 Params
  18 +%p= pretty_hash(notice.params)
  19 +
  20 +%h3 Session
  21 +%p= pretty_hash(notice.session)
  22 +
  23 +%h3 Backtrace
  24 +%table
  25 + - for line in notice.backtrace_lines
  26 + %tr
  27 + %td
  28 + = "#{line.number}:"
  29 + &nbsp;&nbsp;
  30 + %td
  31 + = raw "#{h line.file_relative} -> #{content_tag :strong, h(line.method)}"
  32 +
  33 +%h3 Environment
  34 +%table
  35 + - for key, val in notice.env_vars
  36 + %tr
  37 + %td
  38 + = h key
  39 + %td
  40 + = h val
  41 +
... ...
app/views/notices/_atom_entry.html.haml
... ... @@ -1,41 +0,0 @@
1   -%h2= notice.message
2   -%h3 Summary
3   -- if notice.request['url'].present?
4   - %p
5   - %strong URL:
6   - = link_to(notice.request['url'], notice.request['url'])
7   -%p
8   - %strong Where:
9   - = notice.where
10   -%p
11   - %strong Occured:
12   - = notice.created_at.to_s(:micro)
13   -%p
14   - %strong Similar:
15   - = notice.problem.notices_count - 1
16   -
17   -%h3 Params
18   -%p= pretty_hash(notice.params)
19   -
20   -%h3 Session
21   -%p= pretty_hash(notice.session)
22   -
23   -%h3 Backtrace
24   -%table
25   - - for line in notice.backtrace_lines
26   - %tr
27   - %td
28   - = "#{line.number}:"
29   - &nbsp;&nbsp;
30   - %td
31   - = raw "#{h line.file_relative} -> #{content_tag :strong, h(line.method)}"
32   -
33   -%h3 Environment
34   -%table
35   - - for key, val in notice.env_vars
36   - %tr
37   - %td
38   - = h key
39   - %td
40   - = h val
41   -
app/views/problems/show.html.haml
... ... @@ -3,7 +3,7 @@
3 3 - content_for :title, @problem.error_class || truncate(@problem.message, :length => 32)
4 4 - content_for :meta do
5 5 %strong App:
6   - = link_to @app.name, app_path(@app)
  6 + = link_to @app.name, @app
7 7 %strong Where:
8 8 = @problem.where
9 9 %br
... ... @@ -13,14 +13,14 @@
13 13 = @problem.last_notice_at.to_s(:precise)
14 14 - content_for :action_bar do
15 15 - if @problem.unresolved?
16   - %span= link_to 'resolve', resolve_app_problem_path(@app, @problem), :method => :put, :data => { :confirm => problem_confirm }, :class => 'resolve'
  16 + %span= link_to 'resolve', [:resolve, @app, @problem], :method => :put, :data => { :confirm => problem_confirm }, :class => 'resolve'
17 17 - if current_user.authentication_token
18   - %span= link_to 'iCal', app_problem_path(:app_id => @app.id, :id => @problem.id, :format => "ics", :auth_token => current_user.authentication_token), :class => "calendar_link"
  18 + %span= link_to 'iCal', polymorphic_path([@app, @problem], :format => "ics", :auth_token => current_user.authentication_token), :class => "calendar_link"
19 19 %span>= link_to 'up', (request.env['HTTP_REFERER'] ? :back : app_problems_path(@app)), :class => 'up'
20 20 %br
21 21 = render "issue_tracker_links"
22 22  
23   -- if Errbit::Config.allow_comments_with_issue_tracker || !@app.issue_tracker_configured? || @problem.comments.any?
  23 +- if @problem.comments_allowed? || @problem.comments.any?
24 24 - content_for :comments do
25 25 %h3 Comments
26 26 - @problem.comments.each do |comment|
... ... @@ -33,18 +33,17 @@
33 33 = gravatar_tag comment.user.email, :s => 24
34 34 %span.comment-info
35 35 = time_ago_in_words(comment.created_at, true) << " ago by "
36   - = link_to comment.user.email, user_path(comment.user)
37   - %span.delete= link_to '&#10008;'.html_safe, app_problem_comment_path(@app, @problem, comment), :method => :delete, :data => { :confirm => "Are sure you don't need this comment?" }, :class => "destroy-comment"
  36 + = link_to comment.user.email, comment.user
38 37 - else
39 38 %span.comment-info
40 39 = time_ago_in_words(comment.created_at, true) << " ago by [Unknown User]"
41   - %span.delete= link_to '&#10008;'.html_safe, app_problem_comment_path(@app, @problem, comment), :method => :delete, :data => { :confirm => "Are sure you don't need this comment?" }, :class => "destroy-comment"
  40 + %span.delete= link_to '&#10008;'.html_safe, [@app, @problem, comment], :method => :delete, :data => { :confirm => "Are sure you don't need this comment?" }, :class => "destroy-comment"
42 41 %tr
43   - %td= comment.body.gsub("\n", "<br>").html_safe
44   - - if Errbit::Config.allow_comments_with_issue_tracker || !@app.issue_tracker_configured?
45   - = form_for @comment, :url => app_problem_comments_path(@app, @problem) do |comment_form|
  42 + %td= simple_format comment.body
  43 + - if @problem.comments_allowed?
  44 + = form_for [@app, @problem, @comment] do |comment_form|
46 45 %p Add a comment
47   - = comment_form.text_area :body, :style => "width: 420px; height: 80px;"
  46 + = comment_form.text_area :body
48 47 = comment_form.submit "Save Comment"
49 48  
50 49 %h4= @notice.try(:message)
... ...
config/application.rb
... ... @@ -9,9 +9,12 @@ require &quot;action_mailer/railtie&quot;
9 9 require 'mongoid/railtie'
10 10 require "sprockets/railtie"
11 11  
12   -# If you have a Gemfile, require the gems listed there, including any gems
13   -# you've limited to :test, :development, or :production.
14   -Bundler.require(:default, Rails.env) if defined?(Bundler)
  12 +if defined?(Bundler)
  13 + # If you precompile assets before deploying to production, use this line
  14 + Bundler.require(*Rails.groups(:assets => %w(development test)))
  15 + # If you want your assets lazily compiled in production, use this line
  16 + # Bundler.require(:default, :assets, Rails.env)
  17 +end
15 18  
16 19 module Errbit
17 20 class Application < Rails::Application
... ...
config/config.example.yml
... ... @@ -53,6 +53,8 @@ deployment:
53 53 repository: http://github.com/errbit/errbit.git
54 54 user: deploy
55 55 deploy_to: /var/www/apps/errbit
  56 + # setup path to unicorn pids folder (or deploy_to/shared/pids will be used)
  57 + # pids: /var/www/apps/errbit/shared/pids
56 58  
57 59 # GitHub OAuth configuration
58 60 # If you want to allow authentication via GitHub, you will need to register
... ...
config/deploy.example.rb
... ... @@ -39,6 +39,8 @@ set(:current_branch) { `git branch`.match(/\* (\S+)\s/m)[1] || raise(&quot;Couldn&#39;t d
39 39 set :branch, defer { current_branch }
40 40  
41 41 after 'deploy:update_code', 'errbit:symlink_configs'
  42 +# if unicorn is started through something like runit (the tool which restarts the process when it's stopped)
  43 +# after 'deploy:restart', 'unicorn:stop'
42 44  
43 45 namespace :deploy do
44 46 task :start do ; end
... ... @@ -72,3 +74,24 @@ namespace :db do
72 74 end
73 75 end
74 76  
  77 +namespace :unicorn do
  78 + set(:unicorn_pid) do
  79 + path = config['pids'] || "#{deploy_to}/shared/pids"
  80 + "`cat #{path}/unicorn.pid`"
  81 + end
  82 +
  83 + desc 'Reload unicorn'
  84 + task :reload, :roles => :app, :except => { :no_release => true } do
  85 + run "kill -HUP #{unicorn_pid}"
  86 + end
  87 +
  88 + desc 'Stop unicorn'
  89 + task :stop, :roles => :app, :except => { :no_release => true } do
  90 + run "kill -QUIT #{unicorn_pid}"
  91 + end
  92 +
  93 + desc 'Reexecute unicorn'
  94 + task :reexec, :roles => :app, :except => { :no_release => true } do
  95 + run "kill -USR2 #{unicorn_pid}"
  96 + end
  97 +end
... ...
config/locales/en.yml
... ... @@ -11,3 +11,6 @@ en:
11 11 success: "Good news everyone! '%{app_name}' was successfully updated."
12 12 destroy:
13 13 success: "'%{app_name}' was successfully destroyed."
  14 + n_errs_have:
  15 + one: "%{count} err has"
  16 + other: "%{count} errs have"
... ...
config/mongoid.example.yml
... ... @@ -30,4 +30,4 @@ production:
30 30 port: <%= ENV['MONGOID_PORT'] %>
31 31 username: <%= ENV['MONGOID_USERNAME'] %>
32 32 password: <%= ENV['MONGOID_PASSWORD'] %>
33   - database: <%= ENV['MONGOID_DATABASE'] %>
34 33 \ No newline at end of file
  34 + database: <%= ENV['MONGOID_DATABASE'] %>
... ...
config/newrelic.example.yml
... ... @@ -31,13 +31,13 @@ common: &amp;default_settings
31 31 # into a New Relic "application" on your home dashboard page. If you want
32 32 # to map this instance into multiple apps, like "AJAX Requests" and
33 33 # "All UI" then specify a semicolon-separated list of up to three
34   - # distinct names. If you comment this out, it defaults to the
  34 + # distinct names. If you comment this out, it defaults to the
35 35 # capitalized RAILS_ENV (i.e., Production, Staging, etc)
36 36 app_name: <%= ENV["NEW_RELIC_APP_NAME"] %>
37 37  
38   - # When "true", the agent collects performance data about your
  38 + # When "true", the agent collects performance data about your
39 39 # application and reports this data to the New Relic service at
40   - # newrelic.com. This global switch is normally overridden for each
  40 + # newrelic.com. This global switch is normally overridden for each
41 41 # environment below. (formerly called 'enabled')
42 42 monitor_mode: true
43 43  
... ... @@ -72,11 +72,11 @@ common: &amp;default_settings
72 72  
73 73 # Set your application's Apdex threshold value with the 'apdex_t'
74 74 # setting, in seconds. The apdex_t value determines the buckets used
75   - # to compute your overall Apdex score.
  75 + # to compute your overall Apdex score.
76 76 # Requests that take less than apdex_t seconds to process will be
77 77 # classified as Satisfying transactions; more than apdex_t seconds
78 78 # as Tolerating transactions; and more than four times the apdex_t
79   - # value as Frustrating transactions.
  79 + # value as Frustrating transactions.
80 80 # For more about the Apdex standard, see
81 81 # http://support.newrelic.com/faqs/general/apdex
82 82 apdex_t: 0.5
... ... @@ -97,7 +97,7 @@ common: &amp;default_settings
97 97 # Rails: the RoR filter_parameter_logging excludes parameters
98 98 # Java: create a config setting called "ignored_params" and set it to
99 99 # a comma separated list of HTTP parameter names.
100   - # ex: ignored_params: credit_card, ssn, password
  100 + # ex: ignored_params: credit_card, ssn, password
101 101 capture_params: false
102 102  
103 103 # Transaction tracer captures deep information about slow
... ... @@ -136,7 +136,7 @@ common: &amp;default_settings
136 136 # set to false when using other adapters.
137 137 # explain_enabled: true
138 138  
139   - # Threshold for query execution time below which query plans will not
  139 + # Threshold for query execution time below which query plans will not
140 140 # not be captured. Relevant only when `explain_enabled` is true.
141 141 # explain_threshold: 0.5
142 142  
... ... @@ -149,10 +149,10 @@ common: &amp;default_settings
149 149 # product level.
150 150 enabled: true
151 151  
152   - # Rails Only - tells error collector whether or not to capture a
153   - # source snippet around the place of the error when errors are View
  152 + # Rails Only - tells error collector whether or not to capture a
  153 + # source snippet around the place of the error when errors are View
154 154 # related.
155   - capture_source: true
  155 + capture_source: true
156 156  
157 157 # To stop specific errors from reporting to New Relic, set this property
158 158 # to comma-separated values. Default is to ignore routing errors,
... ... @@ -172,7 +172,7 @@ common: &amp;default_settings
172 172 # disable_memcache_instrumentation: true
173 173 # disable_dj: true
174 174  
175   - # Certain types of instrumentation such as GC stats will not work if
  175 + # Certain types of instrumentation such as GC stats will not work if
176 176 # you are running multi-threaded. Please let us know.
177 177 # multi_threaded = false
178 178  
... ... @@ -190,15 +190,15 @@ development:
190 190 <<: *default_settings
191 191 # Turn off communication to New Relic service in development mode (also
192 192 # 'enabled').
193   - # NOTE: for initial evaluation purposes, you may want to temporarily
  193 + # NOTE: for initial evaluation purposes, you may want to temporarily
194 194 # turn agent communication on in development mode.
195 195 monitor_mode: false
196 196  
197   - # Rails Only - when running in Developer Mode, the New Relic Agent will
  197 + # Rails Only - when running in Developer Mode, the New Relic Agent will
198 198 # present performance information on the last 100 transactions you have
199 199 # executed since starting the app server.
200 200 # NOTE: There is substantial overhead when running in developer mode.
201   - # Do not use for production or load testing.
  201 + # Do not use for production or load testing.
202 202 developer_mode: true
203 203  
204 204 # Enable textmate links
... ...
config/unicorn.rb
... ... @@ -2,4 +2,4 @@
2 2  
3 3 worker_processes 3 # amount of unicorn workers to spin up
4 4 timeout 30 # restarts workers that hang for 30 seconds
5   -preload_app true
6 5 \ No newline at end of file
  6 +preload_app true
... ...
db/migrate/20110422152027_move_notices_to_separate_collection.rb
... ... @@ -5,10 +5,10 @@ class MoveNoticesToSeparateCollection &lt; Mongoid::Migration
5 5 errs = mongo_db.collection("errs").find({ }, :fields => ["notices"])
6 6 errs.each do |err|
7 7 next unless err['notices']
8   -
  8 +
9 9 # This Err was created after the Problem->Err->Notice redesign
10 10 next if err['app_id'].nil? or err['problem_id']
11   -
  11 +
12 12 e = Err.find(err['_id'])
13 13 # disable email notifications
14 14 old_notify = e.app.notify_on_errs?
... ...
db/migrate/20111019163257_add_problem_comments_count.rb
... ... @@ -7,4 +7,4 @@ class AddProblemCommentsCount &lt; Mongoid::Migration
7 7  
8 8 def self.down
9 9 end
10   -end
11 10 \ No newline at end of file
  11 +end
... ...
db/migrate/20111102173347_cache_problem_statistics_fix.rb
... ... @@ -30,4 +30,4 @@ class CacheProblemStatisticsFix &lt; Mongoid::Migration
30 30 counter
31 31 end
32 32  
33   -end
34 33 \ No newline at end of file
  34 +end
... ...
db/migrate/20120822195841_set_first_notice_at_on_problems.rb
... ... @@ -7,4 +7,4 @@ class SetFirstNoticeAtOnProblems &lt; Mongoid::Migration
7 7  
8 8 def self.down
9 9 end
10   -end
11 10 \ No newline at end of file
  11 +end
... ...
db/migrate/20120829034812_ensure_that_problems_last_notice_at_is_not_nil.rb
... ... @@ -2,15 +2,15 @@ class EnsureThatProblemsLastNoticeAtIsNotNil &lt; Mongoid::Migration
2 2 def self.up
3 3 Problem.where("$or" => [{:last_notice_at => nil}, {:first_notice_at => nil}]).each do |problem|
4 4 first_notice = problem.notices.order_by([:created_at, :asc]).first
5   -
  5 +
6 6 # Destroy problems with no notices
7 7 if first_notice.nil?
8 8 problem.destroy
9 9 next
10 10 end
11   -
  11 +
12 12 last_notice = problem.notices.order_by([:created_at, :asc]).last
13   -
  13 +
14 14 problem.update_attributes!({
15 15 :first_notice_at => first_notice.created_at,
16 16 :last_notice_at => last_notice.created_at
... ...
lib/tasks/errbit/bootstrap.rake
1 1 require 'fileutils'
2 2  
3 3 namespace :errbit do
4   -
  4 +
5 5 desc "Copys of example config files"
6 6 task :copy_configs do
7 7 configs = {
... ... @@ -9,7 +9,7 @@ namespace :errbit do
9 9 'deploy.example.rb' => 'deploy.rb',
10 10 (ENV['HEROKU'] ? 'mongoid.mongohq.yml' : 'mongoid.example.yml') => 'mongoid.yml'
11 11 }
12   -
  12 +
13 13 puts "Copying example config files..."
14 14 configs.each do |old, new|
15 15 if File.exists?("config/#{new}")
... ... @@ -20,7 +20,7 @@ namespace :errbit do
20 20 end
21 21 end
22 22 end
23   -
  23 +
24 24 desc "Copy's over example files and seeds the database"
25 25 task :bootstrap do
26 26 Rake::Task['errbit:copy_configs'].execute
... ... @@ -29,5 +29,5 @@ namespace :errbit do
29 29 puts "\n"
30 30 Rake::Task['db:mongoid:create_indexes'].invoke
31 31 end
32   -
33   -end
34 32 \ No newline at end of file
  33 +
  34 +end
... ...
spec/controllers/api/v1/problems_controller_spec.rb
1 1 require 'spec_helper'
2 2  
3   -describe Api::V1::ProblemsController do
4   -
  3 +describe Api::V1::ProblemsController do
  4 +
5 5 context "when logged in" do
6 6 before do
7 7 @user = Fabricate(:user)
8 8 end
9   -
  9 +
10 10 describe "GET /api/v1/problems" do
11 11 before do
12 12 Fabricate(:problem, :first_notice_at => Date.new(2012, 8, 01), :resolved_at => Date.new(2012, 8, 02))
... ... @@ -14,45 +14,45 @@ describe Api::V1::ProblemsController do
14 14 Fabricate(:problem, :first_notice_at => Date.new(2012, 8, 21))
15 15 Fabricate(:problem, :first_notice_at => Date.new(2012, 8, 30))
16 16 end
17   -
18   -
19   -
  17 +
  18 +
  19 +
20 20 it "should return JSON if JSON is requested" do
21 21 get :index, :auth_token => @user.authentication_token, :format => "json"
22 22 lambda { JSON.load(response.body) }.should_not raise_error(JSON::ParserError)
23 23 end
24   -
  24 +
25 25 it "should return XML if XML is requested" do
26 26 get :index, :auth_token => @user.authentication_token, :format => "xml"
27 27 lambda { XML::Parser.string(response.body).parse }.should_not raise_error
28 28 end
29   -
  29 +
30 30 it "should return JSON by default" do
31 31 get :index, :auth_token => @user.authentication_token
32 32 lambda { JSON.load(response.body) }.should_not raise_error(JSON::ParserError)
33 33 end
34   -
35   -
36   -
  34 +
  35 +
  36 +
37 37 describe "given a date range" do
38   -
  38 +
39 39 it "should return only the problems open during the date range" do
40 40 get :index, {:auth_token => @user.authentication_token, :start_date => "2012-08-20", :end_date => "2012-08-27"}
41 41 response.should be_success
42 42 problems = JSON.load response.body
43 43 problems.length.should == 2
44 44 end
45   -
  45 +
46 46 end
47   -
  47 +
48 48 it "should return all problems" do
49 49 get :index, {:auth_token => @user.authentication_token}
50 50 response.should be_success
51 51 problems = JSON.load response.body
52 52 problems.length.should == 4
53 53 end
54   -
  54 +
55 55 end
56 56 end
57   -
  57 +
58 58 end
... ...
spec/controllers/apps_controller_spec.rb
... ... @@ -31,17 +31,6 @@ describe AppsController do
31 31 assigns(:apps).should_not include(unwatched_app)
32 32 end
33 33 end
34   -
35   - context 'when there is only one app' do
36   - it 'sets unresolved_counts and problem_counts variables' do
37   - sign_in Fabricate(:admin)
38   - app = Fabricate(:app)
39   - get :index
40   -
41   - assigns(:unresolved_counts).should == {app.id => 0}
42   - assigns(:problem_counts).should == {app.id => 0}
43   - end
44   - end
45 34 end
46 35  
47 36 describe "GET /apps/:id" do
... ... @@ -261,6 +250,10 @@ describe AppsController do
261 250 end
262 251  
263 252 context "changing email_at_notices" do
  253 + before do
  254 + Errbit::Config.per_app_email_at_notices = true
  255 + end
  256 +
264 257 it "should parse legal csv values" do
265 258 put :update, :id => @app.id, :app => { :email_at_notices => '1, 4, 7,8, 10' }
266 259 @app.reload
... ...
spec/controllers/problems_controller_spec.rb
... ... @@ -419,6 +419,16 @@ describe ProblemsController do
419 419 post :resolve_several, :problems => [@problem2.id.to_s]
420 420 @problem2.reload.resolved?.should == true
421 421 end
  422 +
  423 + it "should display a message about 1 err" do
  424 + post :resolve_several, :problems => [@problem2.id.to_s]
  425 + flash[:success].should match(/1 err has been resolved/)
  426 + end
  427 +
  428 + it "should display a message about 2 errs" do
  429 + post :resolve_several, :problems => [@problem1.id.to_s, @problem2.id.to_s]
  430 + flash[:success].should match(/2 errs have been resolved/)
  431 + end
422 432 end
423 433  
424 434 context "POST /problems/unresolve_several" do
... ...
spec/fabricators/issue_tracker_fabricator.rb
... ... @@ -15,6 +15,10 @@ Fabricator :redmine_tracker, :from =&gt; :issue_tracker, :class_name =&gt; &quot;IssueTrack
15 15 account 'http://redmine.example.com'
16 16 end
17 17  
  18 +Fabricator :gitlab_tracker, :from => :issue_tracker, :class_name => "IssueTrackers::GitlabTracker" do
  19 + account 'http://gitlab.example.com'
  20 +end
  21 +
18 22 Fabricator :mingle_tracker, :from => :issue_tracker, :class_name => "IssueTrackers::MingleTracker" do
19 23 account 'https://mingle.example.com'
20 24 ticket_properties 'card_type = Defect, defect_status = open, priority = essential'
... ...
spec/fixtures/hoptoad_test_notice_with_one_line_of_backtrace.xml
... ... @@ -72,4 +72,4 @@
72 72 <project-root>/path/to/sample/project</project-root>
73 73 <environment-name>development</environment-name>
74 74 </server-environment>
75   -</notice>
76 75 \ No newline at end of file
  76 +</notice>
... ...
spec/interactors/issue_creation_spec.rb 0 → 100644
... ... @@ -0,0 +1,53 @@
  1 +require 'spec_helper'
  2 +
  3 +describe IssueCreation do
  4 + subject(:issue_creation) { IssueCreation.new(problem, user, tracker_name) }
  5 +
  6 + let(:problem) { notice.problem }
  7 + let(:notice) { Fabricate(:notice) }
  8 + let(:user) { Fabricate(:admin) }
  9 + let(:errors) { issue_creation.errors[:base] }
  10 + let(:tracker_name) { nil }
  11 +
  12 + it "adds the error when issue tracker isn't configured" do
  13 + issue_creation.execute
  14 + expect(errors).to include("This app has no issue tracker setup.")
  15 + end
  16 +
  17 + it 'creates an issue if issue tracker is configured' do
  18 + tracker = Fabricate(:lighthouse_tracker, :app => notice.app)
  19 + tracker.should_receive(:create_issue)
  20 + issue_creation.execute
  21 + expect(errors).to be_empty
  22 + end
  23 +
  24 + context "with user's github" do
  25 + let(:tracker_name) { 'user_github' }
  26 +
  27 + it "adds the error when repo isn't set up" do
  28 + issue_creation.execute
  29 + expect(errors).to include("This app doesn't have a GitHub repo set up.")
  30 + end
  31 +
  32 + context 'with repo set up' do
  33 + before do
  34 + notice.app.update_attribute(:github_repo, 'errbit/errbit')
  35 + end
  36 +
  37 + it "adds the error when github account isn't linked" do
  38 + issue_creation.execute
  39 + expect(errors).to include("You haven't linked your Github account.")
  40 + end
  41 +
  42 + it 'creates an issue if github account is linked' do
  43 + user.github_login = 'admin'
  44 + user.github_oauth_token = 'oauthtoken'
  45 + user.save!
  46 +
  47 + GithubIssuesTracker.any_instance.should_receive(:create_issue)
  48 + issue_creation.execute
  49 + expect(errors).to be_empty
  50 + end
  51 + end
  52 + end
  53 +end
... ...
spec/interactors/problem_destroy_spec.rb
... ... @@ -7,7 +7,7 @@ describe ProblemDestroy do
7 7  
8 8 context "in unit way" do
9 9 let(:problem) {
10   - problem = Problem.new()
  10 + problem = Problem.new
11 11 problem.stub(:errs).and_return(mock(:criteria, :only => [err_1, err_2]))
12 12 problem.stub(:comments).and_return(mock(:criteria, :only => [comment_1, comment_2]))
13 13 problem.stub(:delete)
... ...
spec/models/app_spec.rb
... ... @@ -23,6 +23,31 @@ describe App do
23 23 end
24 24 end
25 25  
  26 + describe '<=>' do
  27 + it 'is compared by unresolved count' do
  28 + app_0 = stub_model(App, :name => 'app', :unresolved_count => 1, :problem_count => 1)
  29 + app_1 = stub_model(App, :name => 'app', :unresolved_count => 0, :problem_count => 1)
  30 +
  31 + app_0.should < app_1
  32 + app_1.should > app_0
  33 + end
  34 +
  35 + it 'is compared by problem count' do
  36 + app_0 = stub_model(App, :name => 'app', :unresolved_count => 0, :problem_count => 1)
  37 + app_1 = stub_model(App, :name => 'app', :unresolved_count => 0, :problem_count => 0)
  38 +
  39 + app_0.should < app_1
  40 + app_1.should > app_0
  41 + end
  42 +
  43 + it 'is compared by name' do
  44 + app_0 = stub_model(App, :name => 'app_0', :unresolved_count => 0, :problem_count => 0)
  45 + app_1 = stub_model(App, :name => 'app_1', :unresolved_count => 0, :problem_count => 0)
  46 +
  47 + app_0.should < app_1
  48 + app_1.should > app_0
  49 + end
  50 + end
26 51  
27 52 context 'being created' do
28 53 it 'generates a new api-key' do
... ...
spec/models/issue_trackers/bitbucket_issues_tracker_spec.rb
... ... @@ -10,7 +10,7 @@ describe IssueTrackers::BitbucketIssuesTracker do
10 10  
11 11 number = 123
12 12 @issue_link = "https://bitbucket.org/#{repo}/issue/#{number}/"
13   - body = <<EOF
  13 + body = <<EOF
14 14 {
15 15 "status": "new",
16 16 "priority": "critical",
... ...
spec/models/issue_trackers/fogbugz_tracker_spec.rb
... ... @@ -9,7 +9,7 @@ describe IssueTrackers::FogbugzTracker do
9 9 number = 123
10 10 @issue_link = "https://#{tracker.account}.fogbugz.com/default.asp?#{number}"
11 11 response = "<response><token>12345</token><case><ixBug>123</ixBug></case></response>"
12   - http_mock = mock()
  12 + http_mock = mock
13 13 http_mock.should_receive(:new).and_return(http_mock)
14 14 http_mock.should_receive(:request).twice.and_return(response)
15 15 Fogbugz.adapter[:http] = http_mock
... ...
spec/models/issue_trackers/github_issues_tracker_spec 2.rb 0 → 100644
... ... @@ -0,0 +1,30 @@
  1 +require 'spec_helper'
  2 +
  3 +describe IssueTrackers::GitlabTracker do
  4 + it "should create an issue on Gitlab with problem params" do
  5 + notice = Fabricate :notice
  6 + tracker = Fabricate :gitlab_tracker, :app => notice.app
  7 + problem = notice.problem
  8 +
  9 + number = 5
  10 + @issue_link = "#{tracker.account}/#{tracker.project_id}/issues/#{number}/#{tracker.api_token}"
  11 + body = <<EOF
  12 +{
  13 + "title": "Title"
  14 +}
  15 +EOF
  16 +
  17 + stub_request(:post, "#{tracker.account}/#{tracker.project_id}/issues/#{tracker.api_token}").
  18 + to_return(:status => 201, :headers => {'Location' => @issue_link}, :body => body )
  19 +
  20 + problem.app.issue_tracker.create_issue(problem)
  21 + problem.reload
  22 +
  23 + requested = have_requested(:post, "#{tracker.account}/#{tracker.project_id}/issues/#{tracker.api_token}")
  24 + WebMock.should requested.with(:body => /[production][foo#bar] FooError: Too Much Bar/)
  25 + WebMock.should requested.with(:body => /See this exception on Errbit/)
  26 +
  27 + problem.issue_link.should == @issue_link
  28 + end
  29 +end
  30 +
... ...
spec/models/notice_spec.rb
1 1 require 'spec_helper'
2 2  
3 3 describe Notice do
4   -
5   -
6 4 context 'validations' do
7 5 it 'requires a backtrace' do
8 6 notice = Fabricate.build(:notice, :backtrace => nil)
... ... @@ -23,7 +21,6 @@ describe Notice do
23 21 end
24 22 end
25 23  
26   -
27 24 describe "key sanitization" do
28 25 before do
29 26 @hash = { "some.key" => { "$nested.key" => {"$Path" => "/", "some$key" => "key"}}}
... ... @@ -38,7 +35,6 @@ describe Notice do
38 35 end
39 36 end
40 37  
41   -
42 38 describe "user agent" do
43 39 it "should be parsed and human-readable" do
44 40 notice = Fabricate.build(:notice, :request => {'cgi-data' => {'HTTP_USER_AGENT' => 'Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_6_7; en-US) AppleWebKit/534.16 (KHTML, like Gecko) Chrome/10.0.648.204 Safari/534.16'}})
... ... @@ -80,4 +76,11 @@ describe Notice do
80 76 notice.host.should == 'N/A'
81 77 end
82 78 end
  79 +
  80 + describe "request" do
  81 + it "returns empty hash if not set" do
  82 + notice = Notice.new
  83 + notice.request.should == {}
  84 + end
  85 + end
83 86 end
... ...
spec/models/notification_service/pushover_service_spec.rb
... ... @@ -17,4 +17,4 @@ describe NotificationService::PushoverService do
17 17  
18 18 notification_service.create_notification(problem)
19 19 end
20   -end
21 20 \ No newline at end of file
  21 +end
... ...
spec/models/problem_spec.rb
... ... @@ -24,7 +24,7 @@ describe Problem do
24 24 end
25 25 end
26 26 end
27   -
  27 +
28 28 context '#last_notice_at' do
29 29 it "returns the created_at timestamp of the latest notice" do
30 30 err = Fabricate(:err)
... ... @@ -113,7 +113,7 @@ describe Problem do
113 113 problem = Fabricate(:problem, :notices_count => 1)
114 114 original_notices_count = problem.notices_count
115 115 original_notices_count.should > 0
116   -
  116 +
117 117 problem.resolve!
118 118 problem.notices_count.should == original_notices_count
119 119 end
... ...
spec/views/apps/index.html.haml_spec.rb
... ... @@ -4,8 +4,6 @@ describe &quot;apps/index.html.haml&quot; do
4 4 before do
5 5 app = stub_model(App, :deploys => [stub_model(Deploy, :created_at => Time.now, :revision => "123456789abcdef")])
6 6 assign :apps, [app]
7   - assign :problem_counts, {app.id => 0}
8   - assign :unresolved_counts, {app.id => 0}
9 7 controller.stub(:current_user) { stub_model(User) }
10 8 end
11 9  
... ...