Skip to content

Commit 0288ef9

Browse files
authored
Merge pull request #504 from integer32llc/ci-badge
Add support for showing Travis and Appveyor current build status badges
2 parents 6df510b + 9e297fb commit 0288ef9

24 files changed

+646
-20
lines changed

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ git2 = "0.6"
2424
flate2 = "0.2"
2525
semver = "0.5"
2626
url = "1.2.1"
27-
postgres = { version = "0.13", features = ["with-time", "with-openssl"] }
27+
postgres = { version = "0.13", features = ["with-time", "with-openssl", "with-rustc-serialize"] }
2828
r2d2 = "0.7.0"
2929
r2d2_postgres = "0.11"
3030
openssl = "0.9"

app/components/badge-appveyor.js

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import Ember from 'ember';
2+
3+
export default Ember.Component.extend({
4+
tagName: 'span',
5+
classNames: ['badge'],
6+
repository: Ember.computed.alias('badge.attributes.repository'),
7+
branch: Ember.computed('badge.attributes.branch', function() {
8+
return this.get('badge.attributes.branch') || 'master';
9+
}),
10+
service: Ember.computed('badge.attributes.service', function() {
11+
return this.get('badge.attributes.service') || 'github';
12+
}),
13+
text: Ember.computed('badge', function() {
14+
return `Appveyor build status for the ${ this.get('branch') } branch`;
15+
})
16+
});

app/components/badge-travis-ci.js

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import Ember from 'ember';
2+
3+
export default Ember.Component.extend({
4+
tagName: 'span',
5+
classNames: ['badge'],
6+
repository: Ember.computed.alias('badge.attributes.repository'),
7+
branch: Ember.computed('badge.attributes.branch', function() {
8+
return this.get('badge.attributes.branch') || 'master';
9+
}),
10+
text: Ember.computed('branch', function() {
11+
return `Travis CI build status for the ${ this.get('branch') } branch`;
12+
})
13+
});

app/controllers/crate/version.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ export default Ember.Controller.extend({
2020
requestedVersion: null,
2121
keywords: computed.alias('crate.keywords'),
2222
categories: computed.alias('crate.categories'),
23+
badges: computed.alias('crate.badges'),
2324

2425
sortedVersions: computed.readOnly('crate.versions'),
2526

app/mirage/fixtures/search.js

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,21 @@ export default {
1919
"name": "rust_mixin",
2020
"repository": "https://github.com/huonw/external_mixin",
2121
"updated_at": "2015-02-27T11:52:13Z",
22+
"badges": [
23+
{
24+
"attributes": {
25+
"repository": "huonw/external_mixin"
26+
},
27+
"badge_type": "appveyor"
28+
},
29+
{
30+
"attributes": {
31+
"branch": "master",
32+
"repository": "huonw/external_mixin"
33+
},
34+
"badge_type": "travis-ci"
35+
}
36+
],
2237
"versions": null
2338
}, {
2439
"created_at": "2015-02-27T11:51:58Z",

app/models/crate.js

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import DS from 'ember-data';
2+
import Ember from 'ember';
23

34
export default DS.Model.extend({
45
name: DS.attr('string'),
@@ -17,6 +18,16 @@ export default DS.Model.extend({
1718
license: DS.attr('string'),
1819

1920
versions: DS.hasMany('versions', { async: true }),
21+
badges: DS.attr(),
22+
enhanced_badges: Ember.computed.map('badges', badge => ({
23+
// jshint ignore:start
24+
// needed until https://github.com/jshint/jshint/issues/2991 is fixed
25+
...badge,
26+
// jshint ignore:end
27+
component_name: `badge-${badge.badge_type}`
28+
})),
29+
badge_sort: ['badge_type'],
30+
annotated_badges: Ember.computed.sort('enhanced_badges', 'badge_sort'),
2031
owners: DS.hasMany('users', { async: true }),
2132
version_downloads: DS.hasMany('version-download', { async: true }),
2233
keywords: DS.hasMany('keywords', { async: true }),

app/styles/crate.scss

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -119,9 +119,6 @@
119119
}
120120
.vers {
121121
margin-left: 10px;
122-
img {
123-
margin-bottom: -4px;
124-
}
125122
}
126123

127124
.stats {
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
<a href="https://ci.appveyor.com/project/{{ repository }}">
2+
<img
3+
src="https://ci.appveyor.com/api/projects/status/{{ service }}/{{ repository }}?svg=true&branch={{ branch }}"
4+
alt="{{ text }}"
5+
title="{{ text }}" />
6+
</a>
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
<a href="https://travis-ci.org/{{ repository }}">
2+
<img
3+
src="https://travis-ci.org/{{ repository }}.svg?branch={{ branch }}"
4+
alt="{{ text }}"
5+
title="{{ text }}" />
6+
</a>

app/templates/components/crate-row.hbs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,9 @@
77
alt="{{ crate.max_version }}"
88
title="{{ crate.name }}’s current version badge" />
99
</span>
10+
{{#each crate.annotated_badges as |badge|}}
11+
{{component badge.component_name badge=badge}}
12+
{{/each}}
1013
</div>
1114
<div class='summary'>
1215
<span class='small'>

app/templates/crate/version.hbs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,12 @@
7171
alt="{{ crate.name }}’s current version badge"
7272
title="{{ crate.name }}’s current version badge" />
7373
</p>
74+
75+
{{#each crate.annotated_badges as |badge|}}
76+
<p>
77+
{{component badge.component_name badge=badge}}
78+
</p>
79+
{{/each}}
7480
</div>
7581

7682
<div class="authors">

src/badge.rs

Lines changed: 194 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,194 @@
1+
use util::CargoResult;
2+
use krate::Crate;
3+
use Model;
4+
5+
use std::collections::HashMap;
6+
use pg::GenericConnection;
7+
use pg::rows::Row;
8+
use rustc_serialize::json::Json;
9+
10+
#[derive(Debug, PartialEq, Clone)]
11+
pub enum Badge {
12+
TravisCi {
13+
repository: String, branch: Option<String>,
14+
},
15+
Appveyor {
16+
repository: String, branch: Option<String>, service: Option<String>,
17+
},
18+
}
19+
20+
#[derive(RustcEncodable, RustcDecodable, PartialEq, Debug)]
21+
pub struct EncodableBadge {
22+
pub badge_type: String,
23+
pub attributes: HashMap<String, String>,
24+
}
25+
26+
impl Model for Badge {
27+
fn from_row(row: &Row) -> Badge {
28+
let attributes: Json = row.get("attributes");
29+
if let Json::Object(attributes) = attributes {
30+
let badge_type: String = row.get("badge_type");
31+
match badge_type.as_str() {
32+
"travis-ci" => {
33+
Badge::TravisCi {
34+
branch: attributes.get("branch")
35+
.and_then(Json::as_string)
36+
.map(str::to_string),
37+
repository: attributes.get("repository")
38+
.and_then(Json::as_string)
39+
.map(str::to_string)
40+
.expect("Invalid TravisCi badge \
41+
without repository in the \
42+
database"),
43+
}
44+
},
45+
"appveyor" => {
46+
Badge::Appveyor {
47+
service: attributes.get("service")
48+
.and_then(Json::as_string)
49+
.map(str::to_string),
50+
branch: attributes.get("branch")
51+
.and_then(Json::as_string)
52+
.map(str::to_string),
53+
repository: attributes.get("repository")
54+
.and_then(Json::as_string)
55+
.map(str::to_string)
56+
.expect("Invalid Appveyor badge \
57+
without repository in the \
58+
database"),
59+
}
60+
},
61+
_ => {
62+
panic!("Unknown badge type {} in the database", badge_type);
63+
},
64+
}
65+
} else {
66+
panic!(
67+
"badge attributes {:?} in the database was not a JSON object",
68+
attributes
69+
);
70+
}
71+
}
72+
fn table_name(_: Option<Badge>) -> &'static str { "badges" }
73+
}
74+
75+
impl Badge {
76+
pub fn encodable(self) -> EncodableBadge {
77+
EncodableBadge {
78+
badge_type: self.badge_type().to_string(),
79+
attributes: self.attributes(),
80+
}
81+
}
82+
83+
pub fn badge_type(&self) -> &'static str {
84+
match *self {
85+
Badge::TravisCi {..} => "travis-ci",
86+
Badge::Appveyor {..} => "appveyor",
87+
}
88+
}
89+
90+
pub fn json_attributes(self) -> Json {
91+
Json::Object(self.attributes().into_iter().map(|(k, v)| {
92+
(k, Json::String(v))
93+
}).collect())
94+
}
95+
96+
fn attributes(self) -> HashMap<String, String> {
97+
let mut attributes = HashMap::new();
98+
99+
match self {
100+
Badge::TravisCi { branch, repository } => {
101+
attributes.insert(String::from("repository"), repository);
102+
if let Some(branch) = branch {
103+
attributes.insert(
104+
String::from("branch"),
105+
branch
106+
);
107+
}
108+
},
109+
Badge::Appveyor { service, branch, repository } => {
110+
attributes.insert(String::from("repository"), repository);
111+
if let Some(branch) = branch {
112+
attributes.insert(
113+
String::from("branch"),
114+
branch
115+
);
116+
}
117+
if let Some(service) = service {
118+
attributes.insert(
119+
String::from("service"),
120+
service
121+
);
122+
}
123+
}
124+
}
125+
126+
attributes
127+
}
128+
129+
fn from_attributes(badge_type: &str,
130+
attributes: &HashMap<String, String>)
131+
-> Result<Badge, String> {
132+
match badge_type {
133+
"travis-ci" => {
134+
match attributes.get("repository") {
135+
Some(repository) => {
136+
Ok(Badge::TravisCi {
137+
repository: repository.to_string(),
138+
branch: attributes.get("branch")
139+
.map(String::to_string),
140+
})
141+
},
142+
None => Err(badge_type.to_string()),
143+
}
144+
},
145+
"appveyor" => {
146+
match attributes.get("repository") {
147+
Some(repository) => {
148+
Ok(Badge::Appveyor {
149+
repository: repository.to_string(),
150+
branch: attributes.get("branch")
151+
.map(String::to_string),
152+
service: attributes.get("service")
153+
.map(String::to_string),
154+
155+
})
156+
},
157+
None => Err(badge_type.to_string()),
158+
}
159+
},
160+
_ => Err(badge_type.to_string()),
161+
}
162+
}
163+
164+
pub fn update_crate(conn: &GenericConnection,
165+
krate: &Crate,
166+
badges: HashMap<String, HashMap<String, String>>)
167+
-> CargoResult<Vec<String>> {
168+
169+
let mut invalid_badges = vec![];
170+
171+
let badges: Vec<_> = badges.iter().filter_map(|(k, v)| {
172+
Badge::from_attributes(k, v).map_err(|invalid_badge| {
173+
invalid_badges.push(invalid_badge)
174+
}).ok()
175+
}).collect();
176+
177+
conn.execute("\
178+
DELETE FROM badges \
179+
WHERE crate_id = $1;",
180+
&[&krate.id]
181+
)?;
182+
183+
for badge in badges {
184+
conn.execute("\
185+
INSERT INTO badges (crate_id, badge_type, attributes) \
186+
VALUES ($1, $2, $3) \
187+
ON CONFLICT (crate_id, badge_type) DO UPDATE \
188+
SET attributes = EXCLUDED.attributes;",
189+
&[&krate.id, &badge.badge_type(), &badge.json_attributes()]
190+
)?;
191+
}
192+
Ok(invalid_badges)
193+
}
194+
}

src/bin/migrate.rs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -819,6 +819,18 @@ fn migrations() -> Vec<Migration> {
819819
ON crates_categories;"));
820820
Ok(())
821821
}),
822+
Migration::add_table(20170102131034, "badges", " \
823+
crate_id INTEGER NOT NULL, \
824+
badge_type VARCHAR NOT NULL, \
825+
attributes JSONB NOT NULL"),
826+
Migration::new(20170102145236, |tx| {
827+
try!(tx.execute("CREATE UNIQUE INDEX badges_crate_type \
828+
ON badges (crate_id, badge_type)", &[]));
829+
Ok(())
830+
}, |tx| {
831+
try!(tx.execute("DROP INDEX badges_crate_type", &[]));
832+
Ok(())
833+
}),
822834
];
823835
// NOTE: Generate a new id via `date +"%Y%m%d%H%M%S"`
824836

0 commit comments

Comments
 (0)