superconductor 0.0.4 → 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +16 -0
- data/.rspec +3 -0
- data/.ruby-version +1 -0
- data/.scss-lint.yml +21 -0
- data/.travis.yml +5 -0
- data/.yardopts +1 -0
- data/Cargo.lock +679 -0
- data/Cargo.toml +25 -0
- data/Gemfile +4 -0
- data/Guardfile +70 -0
- data/LICENSE.txt +21 -0
- data/Makefile +4 -0
- data/README.md +58 -0
- data/Rakefile +9 -36
- data/assets/__pm.js +424 -0
- data/assets/__pm.scss +261 -0
- data/assets/_buttons.scss +50 -0
- data/assets/_checkbox.scss +59 -0
- data/assets/_colors.scss +15 -0
- data/assets/_details.scss +54 -0
- data/assets/_pm_commits.scss +171 -0
- data/assets/_pm_setup.scss +7 -0
- data/assets/_pm_tasks.scss +280 -0
- data/assets/_tokens.scss +56 -0
- data/bin/console +14 -0
- data/bin/make +2 -0
- data/bin/rake +17 -0
- data/bin/release +5 -0
- data/bin/setup +8 -0
- data/bin/start +5 -0
- data/config.ru +10 -0
- data/config/properties.yml +37 -0
- data/extconf.rb +2 -0
- data/lib/superconductor.rb +35 -1
- data/lib/superconductor/documentation.rb +34 -0
- data/lib/superconductor/middleware.rb +71 -0
- data/lib/superconductor/version.rb +1 -1
- data/src/lib.rs +101 -0
- data/src/server.rs +176 -0
- data/src/state/mod.rs +5 -0
- data/src/state/state.rs +233 -0
- data/src/state/xml.rs +380 -0
- data/src/task.rs +129 -0
- data/src/view.rs +396 -0
- data/superconductor.gemspec +41 -0
- metadata +173 -116
- data/MIT-LICENSE +0 -20
- data/README.rdoc +0 -3
- data/app/assets/javascripts/jquery-linedtextarea.js +0 -126
- data/app/assets/javascripts/superconductor.js +0 -2
- data/app/assets/javascripts/superconductor/panel.js.coffee +0 -42
- data/app/assets/stylesheets/jquery-linedtextarea.css +0 -68
- data/app/assets/stylesheets/scaffold.css +0 -56
- data/app/assets/stylesheets/superconductor.css +0 -4
- data/app/assets/stylesheets/superconductor/panel.css.scss +0 -142
- data/app/controllers/file_controller.rb +0 -24
- data/app/helpers/superconductor/panel_helper.rb +0 -2
- data/app/models/superconductor.rb +0 -5
- data/app/models/superconductor/exception.rb +0 -0
- data/app/views/superconductor/_panel.html.erb +0 -130
- data/app/views/superconductor/_panel.js.erb +0 -0
- data/config/routes.rb +0 -6
- data/lib/superconductor/engine.rb +0 -24
- data/lib/tasks/superconductor_tasks.rake +0 -4
- data/test/dummy/README.rdoc +0 -261
- data/test/dummy/Rakefile +0 -7
- data/test/dummy/app/assets/javascripts/application.js +0 -15
- data/test/dummy/app/assets/stylesheets/application.css +0 -13
- data/test/dummy/app/controllers/application_controller.rb +0 -3
- data/test/dummy/app/helpers/application_helper.rb +0 -2
- data/test/dummy/app/views/layouts/application.html.erb +0 -14
- data/test/dummy/config.ru +0 -4
- data/test/dummy/config/application.rb +0 -56
- data/test/dummy/config/boot.rb +0 -10
- data/test/dummy/config/database.yml +0 -25
- data/test/dummy/config/environment.rb +0 -5
- data/test/dummy/config/environments/development.rb +0 -37
- data/test/dummy/config/environments/production.rb +0 -67
- data/test/dummy/config/environments/test.rb +0 -37
- data/test/dummy/config/initializers/backtrace_silencers.rb +0 -7
- data/test/dummy/config/initializers/inflections.rb +0 -15
- data/test/dummy/config/initializers/mime_types.rb +0 -5
- data/test/dummy/config/initializers/secret_token.rb +0 -7
- data/test/dummy/config/initializers/session_store.rb +0 -8
- data/test/dummy/config/initializers/wrap_parameters.rb +0 -14
- data/test/dummy/config/locales/en.yml +0 -5
- data/test/dummy/config/routes.rb +0 -58
- data/test/dummy/public/404.html +0 -26
- data/test/dummy/public/422.html +0 -26
- data/test/dummy/public/500.html +0 -25
- data/test/dummy/public/favicon.ico +0 -0
- data/test/dummy/script/rails +0 -6
- data/test/integration/navigation_test.rb +0 -10
- data/test/superconductor_test.rb +0 -7
- data/test/test_helper.rb +0 -10
- data/test/unit/helpers/superconductor/panel_helper_test.rb +0 -4
data/src/state/xml.rs
ADDED
@@ -0,0 +1,380 @@
|
|
1
|
+
use state::State;
|
2
|
+
use task::Task;
|
3
|
+
|
4
|
+
use std::cell::RefCell;
|
5
|
+
use std::fs::File;
|
6
|
+
use std::io::prelude::*;
|
7
|
+
use std::env;
|
8
|
+
use termion::color;
|
9
|
+
|
10
|
+
extern crate git2;
|
11
|
+
use self::git2::Repository;
|
12
|
+
use self::git2::StatusOptions;
|
13
|
+
use self::git2::Delta;
|
14
|
+
use self::git2::BranchType;
|
15
|
+
use self::git2::{Diff, DiffDelta, DiffHunk, DiffLine, DiffBinary};
|
16
|
+
use git2::ObjectType;
|
17
|
+
use state::Filter;
|
18
|
+
|
19
|
+
extern crate md5;
|
20
|
+
extern crate chrono;
|
21
|
+
use maud::PreEscaped;
|
22
|
+
use self::chrono::{TimeZone, FixedOffset};
|
23
|
+
|
24
|
+
use yaml_rust::Yaml;
|
25
|
+
|
26
|
+
extern crate base64;
|
27
|
+
|
28
|
+
use self::base64::encode;
|
29
|
+
|
30
|
+
pub fn generate(state: Option<State>) -> String {
|
31
|
+
let repo = match Repository::open_from_env() {
|
32
|
+
Ok(repo) => repo,
|
33
|
+
Err(_) => Repository::init(env::var("GIT_DIR").unwrap_or(String::from("."))).unwrap()
|
34
|
+
};
|
35
|
+
println!("Generating from repo: {:?}", repo.path());
|
36
|
+
let config = repo.config().unwrap();
|
37
|
+
|
38
|
+
// If there is no master branch, start the setup
|
39
|
+
if repo.find_branch("master", BranchType::Local).is_err() {
|
40
|
+
return html! {
|
41
|
+
state {
|
42
|
+
setup "1"
|
43
|
+
@if let Some(state) = state {
|
44
|
+
message (state.message)
|
45
|
+
task {
|
46
|
+
name "master"
|
47
|
+
@for property in state.property {
|
48
|
+
property {
|
49
|
+
name (property.name)
|
50
|
+
value (property.value)
|
51
|
+
}
|
52
|
+
}
|
53
|
+
}
|
54
|
+
} @else {
|
55
|
+
task {
|
56
|
+
name "master"
|
57
|
+
}
|
58
|
+
}
|
59
|
+
(project())
|
60
|
+
}
|
61
|
+
}.into_string()
|
62
|
+
}
|
63
|
+
|
64
|
+
let head = repo.head().unwrap();
|
65
|
+
let head_tree_obj = head.peel(ObjectType::Tree).unwrap();
|
66
|
+
let head_tree = head_tree_obj.as_tree().unwrap();
|
67
|
+
let changes = repo.diff_tree_to_index(Some(&head_tree), None, None).unwrap();
|
68
|
+
|
69
|
+
let branches = repo.branches(Some(BranchType::Local)).unwrap().filter_map(|b|b.ok());
|
70
|
+
let all_tasks: Vec<Task> = branches.filter_map(|(branch, _)| {
|
71
|
+
let task = Task::from_ref(branch.get());
|
72
|
+
if let Some(ref state) = state {
|
73
|
+
if task.name != state.task { Some(task) } else { None }
|
74
|
+
} else {
|
75
|
+
if !branch.is_head() { Some(task) } else { None }
|
76
|
+
}
|
77
|
+
}).collect();
|
78
|
+
|
79
|
+
// Set Filters
|
80
|
+
let filter: Option<Filter> = match all_tasks.is_empty() {
|
81
|
+
true => Some(Filter {
|
82
|
+
name: String::from("Status"),
|
83
|
+
value: String::from("Sprint")
|
84
|
+
}),
|
85
|
+
false => match state {
|
86
|
+
Some(ref state) => match state.filter {
|
87
|
+
Some(ref filter) if filter.name != "" => state.filter.clone(),
|
88
|
+
_ => None,
|
89
|
+
},
|
90
|
+
None => None
|
91
|
+
}
|
92
|
+
};
|
93
|
+
|
94
|
+
// Apply Filters
|
95
|
+
let mut tasks: Vec<&Task> = match filter {
|
96
|
+
Some(ref filter) => {
|
97
|
+
let filter_name = Yaml::String(filter.name.clone());
|
98
|
+
let filter_by_value = Yaml::String(filter.value.clone());
|
99
|
+
all_tasks.iter().filter(|task| {
|
100
|
+
match task.get(&repo, &filter_name) {
|
101
|
+
Some(ref value) if *value == filter_by_value => true,
|
102
|
+
Some(ref value) if *value == Yaml::Null => true,
|
103
|
+
None => true,
|
104
|
+
_ => false
|
105
|
+
}
|
106
|
+
}).collect()
|
107
|
+
},
|
108
|
+
None => all_tasks.iter().map(|t|t).collect()
|
109
|
+
};
|
110
|
+
|
111
|
+
tasks.sort_by(|a, b| {
|
112
|
+
let ordinal = Yaml::from_str("Ordinal");
|
113
|
+
let ord_a = a.get(&repo, &ordinal).unwrap_or(Yaml::Real(String::from("1.0"))).as_f64().unwrap_or(1.0);
|
114
|
+
let ord_b = b.get(&repo, &ordinal).unwrap_or(Yaml::Real(String::from("1.0"))).as_f64().unwrap_or(1.0);
|
115
|
+
ord_a.partial_cmp(&ord_b).unwrap()
|
116
|
+
});
|
117
|
+
|
118
|
+
let branch = match state.clone() {
|
119
|
+
Some(state) => match repo.find_branch(&state.task, BranchType::Local) {
|
120
|
+
Ok(branch) => branch.into_reference(),
|
121
|
+
Err(_) => head
|
122
|
+
},
|
123
|
+
None => head
|
124
|
+
};
|
125
|
+
|
126
|
+
let task = Task::from_ref(&branch);
|
127
|
+
|
128
|
+
let mut revwalk = repo.revwalk().unwrap();
|
129
|
+
revwalk.set_sorting(git2::SORT_REVERSE);
|
130
|
+
revwalk.push(branch.target().unwrap()).unwrap();
|
131
|
+
if branch.shorthand().unwrap() != "master" {
|
132
|
+
revwalk.hide_ref("refs/heads/master").unwrap();
|
133
|
+
}
|
134
|
+
|
135
|
+
let mut status_opts = StatusOptions::new();
|
136
|
+
status_opts.include_untracked(true);
|
137
|
+
|
138
|
+
let payload = html! {
|
139
|
+
state {
|
140
|
+
@if let Some(commit) = state.clone() {
|
141
|
+
focus (commit.focus)
|
142
|
+
}
|
143
|
+
user {
|
144
|
+
name (config.get_string("user.name" ).unwrap_or(String::from("Unknown")))
|
145
|
+
email (config.get_string("user.email").unwrap_or(String::from("root@localhost")))
|
146
|
+
}
|
147
|
+
@if let Some(state) = state.clone() {
|
148
|
+
message (state.message)
|
149
|
+
@if state.property.len() == 0 {
|
150
|
+
@let task = Task::from_ref(&branch) {
|
151
|
+
(render_task(&repo, &task, task.properties(&repo)))
|
152
|
+
}
|
153
|
+
} @else {
|
154
|
+
task {
|
155
|
+
name (state.task)
|
156
|
+
@for property in state.property {
|
157
|
+
property {
|
158
|
+
name (property.name)
|
159
|
+
value (property.value)
|
160
|
+
}
|
161
|
+
}
|
162
|
+
}
|
163
|
+
}
|
164
|
+
} @else {
|
165
|
+
@let task = Task::from_ref(&branch) {
|
166
|
+
(render_task(&repo, &task, task.properties(&repo)))
|
167
|
+
}
|
168
|
+
}
|
169
|
+
tasks {
|
170
|
+
@if let Some(filter) = filter {
|
171
|
+
@if filter.name != "" {
|
172
|
+
filter {
|
173
|
+
name (filter.name)
|
174
|
+
value (filter.value)
|
175
|
+
}
|
176
|
+
}
|
177
|
+
}
|
178
|
+
@for task in tasks {
|
179
|
+
(render_task(&repo, &task, task.properties(&repo)))
|
180
|
+
}
|
181
|
+
}
|
182
|
+
log {
|
183
|
+
@for (_, rev) in revwalk.enumerate() {
|
184
|
+
@let commit = repo.find_commit(rev.unwrap()).unwrap() {
|
185
|
+
commit {
|
186
|
+
id (commit.id())
|
187
|
+
@let time = commit.time() {
|
188
|
+
timestamp (time.seconds())
|
189
|
+
localtime (FixedOffset::east(time.offset_minutes()*60).timestamp(time.seconds(), 0).to_rfc3339())
|
190
|
+
}
|
191
|
+
user {
|
192
|
+
@let author = commit.author() {
|
193
|
+
name (author.name().unwrap())
|
194
|
+
@let email = author.email().unwrap().trim() {
|
195
|
+
email (email)
|
196
|
+
image (format!("https://www.gravatar.com/avatar/{:x}?s=64", md5::compute(email.to_lowercase())))
|
197
|
+
}
|
198
|
+
}
|
199
|
+
}
|
200
|
+
@let mut message = commit.message().unwrap().split("---\n") {
|
201
|
+
message (message.next().unwrap())
|
202
|
+
@let task = Task::from_commit(&branch.shorthand().unwrap(), &commit) {
|
203
|
+
(render_task(&repo, &task, task.changes(&repo)))
|
204
|
+
}
|
205
|
+
}
|
206
|
+
}
|
207
|
+
}
|
208
|
+
}
|
209
|
+
}
|
210
|
+
@if task.name == "master" {
|
211
|
+
(project())
|
212
|
+
} @else {
|
213
|
+
(properties())
|
214
|
+
}
|
215
|
+
changes {
|
216
|
+
@if let Ok(delta) = changes.stats() {
|
217
|
+
@if delta.files_changed() + delta.insertions() + delta.deletions() > 0 {
|
218
|
+
statistics {
|
219
|
+
files (delta.files_changed())
|
220
|
+
insertions (delta.insertions())
|
221
|
+
deletions (delta.deletions())
|
222
|
+
}
|
223
|
+
}
|
224
|
+
}
|
225
|
+
@for change in repo.statuses(Some(&mut status_opts)).unwrap().iter() {
|
226
|
+
@let path = change.path().unwrap() {
|
227
|
+
change id=(path.replace("/", "_").replace(".", "_").replace(" ", "_")) {
|
228
|
+
path (path)
|
229
|
+
insertions {}
|
230
|
+
deletions {}
|
231
|
+
included @match change.head_to_index().map(|d| d.status()).unwrap_or(Delta::Unreadable) {
|
232
|
+
Delta::Modified | Delta::Added | Delta::Deleted => "true",
|
233
|
+
_ => "false"
|
234
|
+
}
|
235
|
+
removal @match change.head_to_index().map(|d| d.status()).unwrap_or(Delta::Unreadable) {
|
236
|
+
Delta::Deleted => "true",
|
237
|
+
_ => "false"
|
238
|
+
}
|
239
|
+
}
|
240
|
+
}
|
241
|
+
}
|
242
|
+
}
|
243
|
+
diffs {
|
244
|
+
@for change in diff(changes) {
|
245
|
+
(change)
|
246
|
+
}
|
247
|
+
}
|
248
|
+
}
|
249
|
+
}.into_string();
|
250
|
+
println!(" {}Sent payload of size: {}{}", color::Fg(color::LightGreen), payload.len(), color::Fg(color::Reset));
|
251
|
+
payload
|
252
|
+
}
|
253
|
+
|
254
|
+
fn diff(changes: Diff) -> Vec<PreEscaped<String>> {
|
255
|
+
let result = RefCell::new(vec![]);
|
256
|
+
changes.foreach(&mut |delta: DiffDelta, _: f32| {
|
257
|
+
result.borrow_mut().push(html!(
|
258
|
+
@if let Some(path) = delta.new_file().path() {
|
259
|
+
label (path.to_str().unwrap_or("[invalid]"))
|
260
|
+
}
|
261
|
+
));
|
262
|
+
true
|
263
|
+
}, Some(&mut |delta: DiffDelta, binary: DiffBinary| {
|
264
|
+
if binary.contains_data() {
|
265
|
+
result.borrow_mut().push(html!(
|
266
|
+
img src=(format!("data:image/png,{}", (String::from_utf8_lossy(&binary.new_file().data())))) {}
|
267
|
+
));
|
268
|
+
} else {
|
269
|
+
let path = delta.new_file().path().unwrap();
|
270
|
+
let mut file = File::open(path).unwrap();
|
271
|
+
let mut contents = vec![];
|
272
|
+
file.read_to_end(&mut contents).unwrap();
|
273
|
+
result.borrow_mut().push(html!(
|
274
|
+
img src=(format!("data:image/jpeg;base64,{}", encode(&contents))) alt=(format!("{}", path.to_str().unwrap())) {}
|
275
|
+
));
|
276
|
+
}
|
277
|
+
true
|
278
|
+
}), None, Some(&mut |_: DiffDelta, _: Option<DiffHunk>, line: DiffLine| {
|
279
|
+
let class = match line.origin() {
|
280
|
+
'+' | '>' => "add",
|
281
|
+
'-' | '<' => "sub",
|
282
|
+
'H' | 'F' => "meta",
|
283
|
+
_ => ""
|
284
|
+
};
|
285
|
+
result.borrow_mut().push(html!(
|
286
|
+
span class=(class) (String::from_utf8_lossy(&line.content()))
|
287
|
+
));
|
288
|
+
true
|
289
|
+
})).unwrap();
|
290
|
+
result.into_inner()
|
291
|
+
}
|
292
|
+
|
293
|
+
fn render_task(repo: &Repository, task: &Task, changes: Vec<(String, Option<String>, String)>) -> PreEscaped<String> {
|
294
|
+
html!(task {
|
295
|
+
name (task.name)
|
296
|
+
@for (name, before, value) in changes {
|
297
|
+
property {
|
298
|
+
name (name)
|
299
|
+
@if let Some(before) = before {
|
300
|
+
before (before)
|
301
|
+
}
|
302
|
+
value (value)
|
303
|
+
}
|
304
|
+
}
|
305
|
+
})
|
306
|
+
}
|
307
|
+
|
308
|
+
fn properties() -> PreEscaped<String> {
|
309
|
+
html! {
|
310
|
+
properties {
|
311
|
+
property {
|
312
|
+
name "Status"
|
313
|
+
options {
|
314
|
+
option "Sprint"
|
315
|
+
option "In Progress"
|
316
|
+
option "In Review"
|
317
|
+
option "Blocked"
|
318
|
+
option "Done"
|
319
|
+
}
|
320
|
+
}
|
321
|
+
property {
|
322
|
+
name "Ordinal"
|
323
|
+
value "1.0"
|
324
|
+
}
|
325
|
+
property {
|
326
|
+
name "Estimate"
|
327
|
+
}
|
328
|
+
property {
|
329
|
+
name "Developer"
|
330
|
+
value "Jaden Carver <jaden.carver@gmail.com>"
|
331
|
+
options {
|
332
|
+
option value="Jaden Carver <jaden.carver@gmail.com>" "Jaden Carver"
|
333
|
+
option value="Bob Dole <bdole69@gmail.com>" "Bob Dole"
|
334
|
+
}
|
335
|
+
}
|
336
|
+
property {
|
337
|
+
name "Manager"
|
338
|
+
value "Jaden Carver <jaden.carver@gmail.com>"
|
339
|
+
options {
|
340
|
+
option value="Jaden Carver <jaden.carver@gmail.com>" "Jaden Carver"
|
341
|
+
option value="Bob Dole <bdole69@gmail.com>" "Bob Dole"
|
342
|
+
}
|
343
|
+
}
|
344
|
+
property {
|
345
|
+
name "Description"
|
346
|
+
}
|
347
|
+
}
|
348
|
+
}
|
349
|
+
}
|
350
|
+
|
351
|
+
fn project() -> PreEscaped<String> {
|
352
|
+
html! {
|
353
|
+
properties {
|
354
|
+
property {
|
355
|
+
name "Project"
|
356
|
+
}
|
357
|
+
property {
|
358
|
+
name "Status"
|
359
|
+
options {
|
360
|
+
option "Sprint"
|
361
|
+
option "In Progress"
|
362
|
+
option "In Review"
|
363
|
+
option "Blocked"
|
364
|
+
option "Done"
|
365
|
+
}
|
366
|
+
}
|
367
|
+
property {
|
368
|
+
name "Manager"
|
369
|
+
value "Jaden Carver <jaden.carver@gmail.com>"
|
370
|
+
options {
|
371
|
+
option value="Jaden Carver <jaden.carver@gmail.com>" "Jaden Carver"
|
372
|
+
option value="Bob Dole <bdole69@gmail.com>" "Bob Dole"
|
373
|
+
}
|
374
|
+
}
|
375
|
+
property {
|
376
|
+
name "Description"
|
377
|
+
}
|
378
|
+
}
|
379
|
+
}
|
380
|
+
}
|
data/src/task.rs
ADDED
@@ -0,0 +1,129 @@
|
|
1
|
+
use yaml_rust::{Yaml, YamlLoader};
|
2
|
+
use yaml_rust::yaml::Hash;
|
3
|
+
|
4
|
+
extern crate git2;
|
5
|
+
use self::git2::{Repository, ObjectType};
|
6
|
+
use self::git2::{Commit, Reference, Oid};
|
7
|
+
|
8
|
+
#[derive(Debug)]
|
9
|
+
pub struct Task {
|
10
|
+
pub name: String,
|
11
|
+
commit: Option<Oid>,
|
12
|
+
changes: Hash
|
13
|
+
}
|
14
|
+
|
15
|
+
impl Task {
|
16
|
+
pub fn from_ref(reference: &Reference) -> Task {
|
17
|
+
let commit_obj = reference.peel(ObjectType::Commit).unwrap();
|
18
|
+
let commit = commit_obj.as_commit().unwrap();
|
19
|
+
let name = reference.shorthand().unwrap_or("master");
|
20
|
+
Task::from_commit(&name, &commit)
|
21
|
+
}
|
22
|
+
|
23
|
+
pub fn from_commit(name: &str, commit: &Commit) -> Task {
|
24
|
+
let mut messages = commit.message().unwrap().split("---\n");
|
25
|
+
let _message = messages.next().unwrap();
|
26
|
+
let mut tasks = vec![];
|
27
|
+
if let Some(yaml) = messages.next() {
|
28
|
+
if let Ok(loader) = YamlLoader::load_from_str(yaml) {
|
29
|
+
for yaml in loader.iter() {
|
30
|
+
if let Some(hash) = yaml.as_hash() {
|
31
|
+
for (key, values) in hash {
|
32
|
+
if let Some(key) = key.as_str() {
|
33
|
+
if let Some(values) = values.as_hash() {
|
34
|
+
tasks.push(Task {
|
35
|
+
name: String::from(key),
|
36
|
+
changes: values.clone(),
|
37
|
+
commit: Some(commit.id())
|
38
|
+
});
|
39
|
+
}
|
40
|
+
}
|
41
|
+
}
|
42
|
+
}
|
43
|
+
}
|
44
|
+
}
|
45
|
+
};
|
46
|
+
tasks.retain(|c| c.name == name);
|
47
|
+
tasks.pop().unwrap_or(Task {
|
48
|
+
name: String::from(name),
|
49
|
+
commit: Some(commit.id()),
|
50
|
+
changes: Hash::new()
|
51
|
+
})
|
52
|
+
}
|
53
|
+
|
54
|
+
pub fn get(&self, repo: &Repository, property: &Yaml) -> Option<Yaml> {
|
55
|
+
if let Some(value) = self.changes.get(&property) {
|
56
|
+
Some(value.clone())
|
57
|
+
} else if let Some(commit) = self.commit {
|
58
|
+
let commit = repo.find_commit(commit).expect("Unable to find commit!");
|
59
|
+
let parents = commit.parents().map(|parent| Task::from_commit(&self.name, &parent));
|
60
|
+
let mut candidates: Vec<Yaml> = parents.filter_map(|task| task.get(&repo, property)).collect();
|
61
|
+
candidates.pop()
|
62
|
+
} else {
|
63
|
+
None
|
64
|
+
}
|
65
|
+
}
|
66
|
+
|
67
|
+
pub fn parent(&self, repo: &Repository) -> Option<Task> {
|
68
|
+
if let Some(commit_oid) = self.commit {
|
69
|
+
let commit = repo.find_commit(commit_oid).unwrap();
|
70
|
+
if let Some(parent) = commit.parents().next() {
|
71
|
+
Some(Task::from_commit(&self.name, &parent))
|
72
|
+
} else { None }
|
73
|
+
} else { None }
|
74
|
+
}
|
75
|
+
|
76
|
+
pub fn changes(&self, repo: &Repository) -> Vec<(String, Option<String>, String)> {
|
77
|
+
let mut changes = vec![];
|
78
|
+
for (key, value) in self.changes.clone() {
|
79
|
+
let after = match value {
|
80
|
+
Yaml::String(ref s) => s.clone(),
|
81
|
+
Yaml::Integer(i) => format!("{}", i),
|
82
|
+
Yaml::Real(ref i) => format!("{}", i),
|
83
|
+
Yaml::Boolean(b) => format!("{}", b),
|
84
|
+
Yaml::Null => format!(""),
|
85
|
+
_ => String::from("[unknown]")
|
86
|
+
};
|
87
|
+
let before = if let Some(parent) = self.parent(&repo) {
|
88
|
+
match parent.get(&repo, &key) {
|
89
|
+
Some(before_value) => {
|
90
|
+
if before_value == value {
|
91
|
+
None
|
92
|
+
} else {
|
93
|
+
match before_value {
|
94
|
+
Yaml::String(ref s) => Some(s.clone()),
|
95
|
+
Yaml::Integer(i) => Some(format!("{}", i)),
|
96
|
+
Yaml::Real(i) => Some(format!("{}", i)),
|
97
|
+
Yaml::Boolean(b) => Some(format!("{}", b)),
|
98
|
+
Yaml::Null => Some(format!("")),
|
99
|
+
_ => None
|
100
|
+
}
|
101
|
+
}
|
102
|
+
}, _ => None
|
103
|
+
}
|
104
|
+
} else { None };
|
105
|
+
changes.push((String::from(key.as_str().unwrap()), before, after));
|
106
|
+
};
|
107
|
+
changes
|
108
|
+
}
|
109
|
+
|
110
|
+
pub fn properties(&self, repo: &Repository) -> Vec<(String, Option<String>, String)> {
|
111
|
+
let mut changes = vec![];
|
112
|
+
let properties = ["Ordinal","Status","Project","Estimate","Developer","Manager","Description"];
|
113
|
+
for property in properties.iter() {
|
114
|
+
let prop = Yaml::String(String::from(*property));
|
115
|
+
if let Some(value) = self.get(&repo, &prop) {
|
116
|
+
let after = match value {
|
117
|
+
Yaml::String(ref s) => s.clone(),
|
118
|
+
Yaml::Integer(i) => format!("{}", i),
|
119
|
+
Yaml::Real(i) => format!("{}", i),
|
120
|
+
Yaml::Boolean(b) => format!("{}", b),
|
121
|
+
Yaml::Null => format!(""),
|
122
|
+
_ => String::from("[unknown]")
|
123
|
+
};
|
124
|
+
changes.push((String::from(*property), None, after));
|
125
|
+
}
|
126
|
+
};
|
127
|
+
changes
|
128
|
+
}
|
129
|
+
}
|