superconductor 0.0.4 → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (97) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +16 -0
  3. data/.rspec +3 -0
  4. data/.ruby-version +1 -0
  5. data/.scss-lint.yml +21 -0
  6. data/.travis.yml +5 -0
  7. data/.yardopts +1 -0
  8. data/Cargo.lock +679 -0
  9. data/Cargo.toml +25 -0
  10. data/Gemfile +4 -0
  11. data/Guardfile +70 -0
  12. data/LICENSE.txt +21 -0
  13. data/Makefile +4 -0
  14. data/README.md +58 -0
  15. data/Rakefile +9 -36
  16. data/assets/__pm.js +424 -0
  17. data/assets/__pm.scss +261 -0
  18. data/assets/_buttons.scss +50 -0
  19. data/assets/_checkbox.scss +59 -0
  20. data/assets/_colors.scss +15 -0
  21. data/assets/_details.scss +54 -0
  22. data/assets/_pm_commits.scss +171 -0
  23. data/assets/_pm_setup.scss +7 -0
  24. data/assets/_pm_tasks.scss +280 -0
  25. data/assets/_tokens.scss +56 -0
  26. data/bin/console +14 -0
  27. data/bin/make +2 -0
  28. data/bin/rake +17 -0
  29. data/bin/release +5 -0
  30. data/bin/setup +8 -0
  31. data/bin/start +5 -0
  32. data/config.ru +10 -0
  33. data/config/properties.yml +37 -0
  34. data/extconf.rb +2 -0
  35. data/lib/superconductor.rb +35 -1
  36. data/lib/superconductor/documentation.rb +34 -0
  37. data/lib/superconductor/middleware.rb +71 -0
  38. data/lib/superconductor/version.rb +1 -1
  39. data/src/lib.rs +101 -0
  40. data/src/server.rs +176 -0
  41. data/src/state/mod.rs +5 -0
  42. data/src/state/state.rs +233 -0
  43. data/src/state/xml.rs +380 -0
  44. data/src/task.rs +129 -0
  45. data/src/view.rs +396 -0
  46. data/superconductor.gemspec +41 -0
  47. metadata +173 -116
  48. data/MIT-LICENSE +0 -20
  49. data/README.rdoc +0 -3
  50. data/app/assets/javascripts/jquery-linedtextarea.js +0 -126
  51. data/app/assets/javascripts/superconductor.js +0 -2
  52. data/app/assets/javascripts/superconductor/panel.js.coffee +0 -42
  53. data/app/assets/stylesheets/jquery-linedtextarea.css +0 -68
  54. data/app/assets/stylesheets/scaffold.css +0 -56
  55. data/app/assets/stylesheets/superconductor.css +0 -4
  56. data/app/assets/stylesheets/superconductor/panel.css.scss +0 -142
  57. data/app/controllers/file_controller.rb +0 -24
  58. data/app/helpers/superconductor/panel_helper.rb +0 -2
  59. data/app/models/superconductor.rb +0 -5
  60. data/app/models/superconductor/exception.rb +0 -0
  61. data/app/views/superconductor/_panel.html.erb +0 -130
  62. data/app/views/superconductor/_panel.js.erb +0 -0
  63. data/config/routes.rb +0 -6
  64. data/lib/superconductor/engine.rb +0 -24
  65. data/lib/tasks/superconductor_tasks.rake +0 -4
  66. data/test/dummy/README.rdoc +0 -261
  67. data/test/dummy/Rakefile +0 -7
  68. data/test/dummy/app/assets/javascripts/application.js +0 -15
  69. data/test/dummy/app/assets/stylesheets/application.css +0 -13
  70. data/test/dummy/app/controllers/application_controller.rb +0 -3
  71. data/test/dummy/app/helpers/application_helper.rb +0 -2
  72. data/test/dummy/app/views/layouts/application.html.erb +0 -14
  73. data/test/dummy/config.ru +0 -4
  74. data/test/dummy/config/application.rb +0 -56
  75. data/test/dummy/config/boot.rb +0 -10
  76. data/test/dummy/config/database.yml +0 -25
  77. data/test/dummy/config/environment.rb +0 -5
  78. data/test/dummy/config/environments/development.rb +0 -37
  79. data/test/dummy/config/environments/production.rb +0 -67
  80. data/test/dummy/config/environments/test.rb +0 -37
  81. data/test/dummy/config/initializers/backtrace_silencers.rb +0 -7
  82. data/test/dummy/config/initializers/inflections.rb +0 -15
  83. data/test/dummy/config/initializers/mime_types.rb +0 -5
  84. data/test/dummy/config/initializers/secret_token.rb +0 -7
  85. data/test/dummy/config/initializers/session_store.rb +0 -8
  86. data/test/dummy/config/initializers/wrap_parameters.rb +0 -14
  87. data/test/dummy/config/locales/en.yml +0 -5
  88. data/test/dummy/config/routes.rb +0 -58
  89. data/test/dummy/public/404.html +0 -26
  90. data/test/dummy/public/422.html +0 -26
  91. data/test/dummy/public/500.html +0 -25
  92. data/test/dummy/public/favicon.ico +0 -0
  93. data/test/dummy/script/rails +0 -6
  94. data/test/integration/navigation_test.rb +0 -10
  95. data/test/superconductor_test.rb +0 -7
  96. data/test/test_helper.rb +0 -10
  97. data/test/unit/helpers/superconductor/panel_helper_test.rb +0 -4
data/extconf.rb ADDED
@@ -0,0 +1,2 @@
1
+ # rust/extconf.rb
2
+ raise 'You have to install Rust with Cargo (https://www.rust-lang.org/)' if !system('cargo --version') || !system('rustc --version')
@@ -1,4 +1,38 @@
1
- require "superconductor/engine"
1
+ require 'fiddle'
2
+ require 'fiddle/import'
2
3
 
3
4
  module Superconductor
5
+ autoload :Middleware, 'superconductor/middleware'
6
+ autoload :Documentation, 'superconductor/documentation'
7
+ autoload :VERSION, 'superconductor/version'
8
+
9
+ target = ENV['TARGET'] || 'release'
10
+ extend Fiddle::Importer
11
+ dlload File.expand_path("../../target/#{target}/libsuperconductor.dylib", __FILE__)
12
+ extern "char* panel_js(int)"
13
+ extern "char* panel_xslt()"
14
+ extern "int start()"
15
+
16
+ #extern "unsigned char u8_return_test()"
17
+ #extern "short i16_return_test()"
18
+ #extern "unsigned short u16_return_test()"
19
+ #extern "int i32_return_test()"
20
+ #extern "unsigned int u32_return_test()"
21
+ #extern "long long i64_return_test()"
22
+ #extern "unsigned long long u64_return_test()"
23
+
24
+ #extern "void i8_pass_test(char)"
25
+ #extern "void u8_pass_test(unsigned char)"
26
+ #extern "void i16_pass_test(short)"
27
+ #extern "void u16_pass_test(unsigned short)"
28
+ #extern "void i32_pass_test(int)"
29
+ #extern "void u32_pass_test(unsigned int)"
30
+ #extern "void i64_pass_test(long long)"
31
+ #extern "void u64_pass_test(unsigned long long)"
32
+
33
+ #extern "float f32_return_test()"
34
+ #extern "double f64_return_test()"
35
+
36
+ #extern "void f32_pass_test(float)"
37
+ #extern "void f64_pass_test(double)"
4
38
  end
@@ -0,0 +1,34 @@
1
+ require 'pry'
2
+ require 'rack/request'
3
+ require 'rack/response'
4
+ require 'superconductor'
5
+
6
+ # Superconductor Documentation Service
7
+ #
8
+ #
9
+ class Superconductor::Documentation
10
+ include Rack::Utils
11
+
12
+ def initialize(path: nil, index: "index.html")
13
+ @path ||= File.join(Dir.getwd, 'doc')
14
+ @index = index
15
+ end
16
+
17
+ def call(env)
18
+ path = env["PATH_INFO"][1..-1]
19
+ file = path == "" ? @index : path
20
+ headers = HeaderHash.new({
21
+ 'Content-Type' => content_type(file)
22
+ })
23
+ body = [open(File.join(@path, file)).read]
24
+ status = 200
25
+ [status, headers, body]
26
+ end
27
+
28
+ private def content_type(file)
29
+ return "text/html" if file =~ /.html?$/
30
+ return "text/css" if file =~ /.css$/
31
+ return "text/javascript" if file =~ /.js$/
32
+ return "text/plain"
33
+ end
34
+ end
@@ -0,0 +1,71 @@
1
+ require 'superconductor'
2
+ require 'pry'
3
+
4
+ module Superconductor
5
+ class Middleware
6
+
7
+ PORT = Superconductor.start();
8
+ ENV["GIT_DIR"] = File.join(Dir.pwd, '.git')
9
+
10
+ def initialize(app)
11
+ @app = app
12
+ @gem_path = File.expand_path(File.join(File.dirname(__FILE__), '../..'))
13
+ @assets = Dir[File.join(@gem_path, 'assets/**/*.{js,css}')]
14
+ @assets += Dir[File.join(@gem_path, 'spec/integration/screenshots/*.png')]
15
+ end
16
+
17
+ def call(env)
18
+ path = env['PATH_INFO']
19
+ serve_asset(path) or serve_xslt(path) or serve_response(env)
20
+ end
21
+
22
+ private
23
+
24
+ def serve_asset(path)
25
+ if asset = @assets.find { |p| path == p[@gem_path.length..-1] }
26
+ status, headers = 200, {}
27
+ body = [open(asset).read]
28
+ headers['Content-Type'] = content_type(path)
29
+ [status, headers, body]
30
+ end
31
+ end
32
+
33
+ def content_type(path)
34
+ case path
35
+ when /.html?$/ then "text/html"
36
+ when /.css$/ then "text/css"
37
+ when /.js$/ then "text/javascript"
38
+ when /.png$/ then "image/png"
39
+ when /.jpg$/ then "image/jpeg"
40
+ else "text/plain"
41
+ end
42
+ end
43
+
44
+ def serve_xslt(path)
45
+ if path == '/__panel.xslt'
46
+ status, headers = 200, {}
47
+ panel_xslt = Superconductor.panel_xslt
48
+ panel_xslt.free = Superconductor[:cleanup]
49
+ body = [panel_xslt.to_s]
50
+ headers['Content-Type'] = 'text/xml'
51
+ [status, headers, body]
52
+ end
53
+ end
54
+
55
+ def serve_response(env)
56
+ status, headers, response = *@app.call(env)
57
+ body = []
58
+ response.each do |res|
59
+ body << res
60
+ end
61
+ if (type = headers["Content-Type"]) && type['text/html']
62
+ panel_js = Superconductor.panel_js(PORT)
63
+ panel_js.free = Superconductor[:cleanup]
64
+ headers.delete('Content-Length')
65
+ body << panel_js.to_s
66
+ end
67
+ [status, headers, body]
68
+ end
69
+
70
+ end
71
+ end
@@ -1,3 +1,3 @@
1
1
  module Superconductor
2
- VERSION = "0.0.4"
2
+ VERSION = "0.1.0"
3
3
  end
data/src/lib.rs ADDED
@@ -0,0 +1,101 @@
1
+ #![feature(plugin)]
2
+ #![plugin(maud_macros)]
3
+
4
+ extern crate maud;
5
+ extern crate websocket;
6
+ extern crate yaml_rust;
7
+ extern crate libc;
8
+
9
+ #[macro_use]
10
+ extern crate serde_derive;
11
+ extern crate serde_xml;
12
+
13
+ use libc::{c_char};
14
+ use std::ffi::CString;
15
+ use std::thread;
16
+
17
+ extern crate rand;
18
+ use rand::Rng;
19
+
20
+ extern crate termion;
21
+
22
+ extern crate git2;
23
+
24
+ pub static XML: &'static str = "<?xml version=\"1.0\" encoding=\"utf-8\"?>";
25
+
26
+
27
+ mod state;
28
+ mod view;
29
+ mod server;
30
+ mod task;
31
+
32
+ #[no_mangle]
33
+ pub extern "C" fn panel_xslt() -> *mut c_char {
34
+ CString::new(view::panel_xslt()).unwrap().into_raw()
35
+ }
36
+
37
+ #[no_mangle]
38
+ pub extern "C" fn start() -> i32 {
39
+ let mut port = 2794;
40
+ let mut _server = server::start(port);
41
+ if _server.is_err() {
42
+ println!("Failed to connect on default websocket port");
43
+ let mut rng = rand::thread_rng();
44
+ port = port + rng.gen::<i32>() % 10;
45
+ _server = server::start(port);
46
+ }
47
+ match _server {
48
+ Ok(server) => {
49
+ thread::spawn(move || {
50
+ for connection in server {
51
+ thread::spawn(move || server::connect(connection.unwrap()));
52
+ }
53
+ });
54
+ },
55
+ _ => println!("Unable to start websocket server")
56
+ };
57
+ port
58
+ }
59
+
60
+ #[no_mangle]
61
+ pub extern "C" fn panel_js(port: i32) -> *mut c_char {
62
+ let markup = html! {
63
+ script { "
64
+ if (window === window.top) {
65
+ //var highlight = document.createElement('script');
66
+ //highlight.setAttribute('src', '//cdnjs.cloudflare.com/ajax/libs/highlight.js/9.10.0/highlight.min.js');
67
+ //document.body.appendChild(highlight);
68
+ PM = document.createElement('script');
69
+ PM.setAttribute('src', '/assets/__pm.js');
70
+ PM.port = " (port) ";
71
+ document.body.appendChild(PM);
72
+ }" }
73
+ };
74
+ CString::new(markup.into_string()).unwrap().into_raw()
75
+ }
76
+
77
+ #[no_mangle]
78
+ pub extern fn cleanup(s: *mut c_char) {
79
+ unsafe {
80
+ if s.is_null() { return }
81
+ CString::from_raw(s)
82
+ };
83
+ }
84
+
85
+ #[cfg(test)]
86
+ mod tests {
87
+ use std::env;
88
+ use std::process::Command;
89
+
90
+ #[test]
91
+ fn it_works() {
92
+ env::set_var("TARGET", "debug");
93
+ env::set_var("GIT_DIR", "tmp/dummy/.git");
94
+
95
+ let rspec = Command::new("rspec")
96
+ .arg("spec/integration")
97
+ .status().expect("Failed to execute rspec");
98
+
99
+ assert!(rspec.success(), "rspec reported failing tests");
100
+ }
101
+ }
data/src/server.rs ADDED
@@ -0,0 +1,176 @@
1
+ use std;
2
+ use std::thread;
3
+ use std::time::Duration;
4
+ use termion::color;
5
+ use termion::clear;
6
+ use termion::cursor;
7
+ use rand;
8
+
9
+ use websocket::{WebSocketStream, Server};
10
+ use websocket::Message as WebMessage;
11
+ use websocket::Sender as WebSender;
12
+ use websocket::client::Sender as WebClientSender;
13
+ use websocket::Receiver as WebReceiver;
14
+ use websocket::message::Type as WebMessageType;
15
+ use websocket::server::Connection;
16
+ use websocket::header::WebSocketProtocol;
17
+
18
+ use state::generate;
19
+ use state::State;
20
+
21
+ extern crate fsevent;
22
+ use self::fsevent::{ITEM_MODIFIED, ITEM_CREATED, ITEM_REMOVED};
23
+ use self::fsevent::Event as FsEvent;
24
+ use std::sync::mpsc::channel;
25
+ use std::sync::mpsc::Sender;
26
+ use std::sync::mpsc::Receiver;
27
+
28
+ use git2::Repository;
29
+
30
+ use serde_xml as xml;
31
+
32
+ const HEARTBEAT: u64 = 250;
33
+
34
+ #[derive(Debug)]
35
+ enum NotifierMessage<'a> {
36
+ WebMessage(WebMessage<'a>),
37
+ FsEvent(FsEvent)
38
+ }
39
+
40
+ pub fn start(port: i32) -> Result<Server<'static>, std::io::Error> {
41
+ let host = format!("127.0.0.1:{}", port);
42
+ Server::bind(host)
43
+ }
44
+
45
+ pub fn connect(connection: Connection<WebSocketStream, WebSocketStream>) {
46
+ let request = connection.read_request().unwrap();
47
+ let headers = request.headers.clone();
48
+ request.validate().unwrap();
49
+
50
+ let mut response = request.accept();
51
+ println!("Connected");
52
+
53
+ if let Some(&WebSocketProtocol(ref protocols)) = headers.get() {
54
+ if protocols.contains(&("superconductor".to_string())) {
55
+ response.headers.set(WebSocketProtocol(vec!["superconductor".to_string()]));
56
+ }
57
+ }
58
+
59
+ let client = response.send().unwrap();
60
+
61
+ let (tx, rx) = channel::<NotifierMessage>();
62
+ let (sender, mut receiver) = client.split();
63
+ let notifier = thread::spawn(move || start_notifier(rx, sender));
64
+
65
+ let message = WebMessage::text(generate(None));
66
+ tx.send(NotifierMessage::WebMessage(message)).unwrap();
67
+
68
+ let monitor_tx = tx.clone();
69
+ let monitor = thread::spawn(move || start_monitor(monitor_tx));
70
+
71
+ let updater_tx = tx.clone();
72
+ let updater = thread::spawn(move || start_updater(updater_tx));
73
+
74
+ for message in receiver.incoming_messages() {
75
+ let message: WebMessage = message.unwrap_or(WebMessage::close());
76
+ match message.opcode {
77
+ WebMessageType::Close => {
78
+ tx.send(NotifierMessage::WebMessage(message)).unwrap();
79
+ break;
80
+ },
81
+ _ => tx.send(NotifierMessage::WebMessage(message))
82
+ }.unwrap();
83
+ }
84
+
85
+ monitor.join().unwrap();
86
+ notifier.join().unwrap();
87
+ updater.join().unwrap();
88
+ }
89
+
90
+ fn start_monitor(tx: Sender<NotifierMessage>) {
91
+ let (ftx, rx) = channel();
92
+ let observer = thread::spawn(move || {
93
+ println!("Monitoring");
94
+ let fsevent = fsevent::FsEvent::new(ftx);
95
+ fsevent.append_path(".");
96
+ fsevent.observe();
97
+ });
98
+ loop {
99
+ let event = rx.recv().unwrap();
100
+ thread::sleep(Duration::from_millis(HEARTBEAT));
101
+ let mut changes = vec![event];
102
+ loop {
103
+ if let Ok(event) = rx.try_recv() {
104
+ if (event.flag.contains(ITEM_MODIFIED) || event.flag.contains(ITEM_CREATED) || event.flag.contains(ITEM_REMOVED)) && (!event.path.contains(".git") || !event.path.contains(".lock")) {
105
+ changes.push(event);
106
+ }
107
+ } else {
108
+ break;
109
+ }
110
+ }
111
+ if !changes.is_empty() {
112
+ if tx.send(NotifierMessage::FsEvent(changes.pop().unwrap())).is_err() {
113
+ break;
114
+ };
115
+ }
116
+ }
117
+ observer.join().unwrap();
118
+ }
119
+
120
+
121
+ fn start_notifier(rx: Receiver<NotifierMessage>, mut sender: WebClientSender<WebSocketStream>) {
122
+ let mut last_state: Option<State> = None;
123
+ let mut rng = rand::thread_rng();
124
+ loop {
125
+ let event = rx.recv().unwrap();
126
+ match event {
127
+ NotifierMessage::WebMessage(message) => {
128
+ match message.opcode {
129
+ WebMessageType::Close => {
130
+ break;
131
+ },
132
+ WebMessageType::Ping => {
133
+ let message = WebMessage::pong(message.payload);
134
+ sender.send_message(&message).unwrap();
135
+ },
136
+ _ => {
137
+ let payload = String::from_utf8_lossy(message.payload.as_ref());
138
+ //println!("\n\n{}{}{}{}{}", clear::All, cursor::Goto(1, 1), color::Fg(color::White), payload, color::Fg(color::Reset));
139
+ let mut state: State = xml::from_str(&payload).unwrap_or(last_state.clone().unwrap_or(State::blank()));
140
+
141
+ last_state = state.apply(last_state, &mut rng).unwrap();
142
+ let message = WebMessage::text(generate(Some(state)));
143
+ sender.send_message(&message).unwrap();
144
+
145
+ thread::sleep(Duration::from_millis(HEARTBEAT));
146
+ flush(&rx);
147
+ }
148
+ }
149
+ },
150
+ NotifierMessage::FsEvent(_event) => {
151
+ //let message = WebMessage::text("submit");
152
+ //sender.send_message(&message).unwrap();
153
+ }
154
+ }
155
+ }
156
+ }
157
+
158
+ fn start_updater(tx: Sender<NotifierMessage>) {
159
+ loop {
160
+ thread::sleep(Duration::from_secs(300));
161
+ let repo = Repository::open_from_env().unwrap_or(Repository::init(".").unwrap());
162
+ if let Ok(mut origin) = repo.find_remote("origin") {
163
+ println!("Updating from remote...");
164
+ origin.fetch(&["master"], None, None);
165
+ };
166
+ }
167
+ }
168
+
169
+ fn flush<T>(channel: &Receiver<T>) {
170
+ let mut flushed: i32 = 0;
171
+ while channel.try_recv().is_ok() {
172
+ flushed += 1;
173
+ }
174
+ let plural = if flushed != 1 { 's' } else { ' ' };
175
+ println!(" {}Flushed {} event{}{}", color::Fg(color::Yellow), flushed, plural, color::Fg(color::Reset));
176
+ }
data/src/state/mod.rs ADDED
@@ -0,0 +1,5 @@
1
+ mod state;
2
+ mod xml;
3
+
4
+ pub use state::state::*;
5
+ pub use state::xml::generate;
@@ -0,0 +1,233 @@
1
+ use std::path::Path;
2
+ use git2::Repository;
3
+ use git2::ObjectType;
4
+ use git2::BranchType;
5
+ use rand::Rng;
6
+ use termion::color;
7
+ use rand;
8
+
9
+ use task::Task;
10
+
11
+ use yaml_rust::yaml::Hash;
12
+ use yaml_rust::{Yaml, YamlEmitter};
13
+
14
+ #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
15
+ pub struct State {
16
+ pub task: String,
17
+ pub focus: String,
18
+ pub filter: Option<Filter>,
19
+ pub message: String,
20
+ pub include: Vec<String>,
21
+ pub property: Vec<Property>,
22
+ pub diff: Vec<String>,
23
+ pub save_update: Option<String>,
24
+ pub new_task: Option<String>,
25
+ pub dragged: Option<String>
26
+ }
27
+
28
+ #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
29
+ pub struct Property {
30
+ pub name: String,
31
+ pub value: String
32
+ }
33
+
34
+ #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
35
+ pub struct Filter {
36
+ pub name: String,
37
+ pub value: String
38
+ }
39
+
40
+ #[derive(Debug)]
41
+ pub enum StateError {
42
+ }
43
+
44
+ impl State {
45
+
46
+ pub fn blank() -> State {
47
+ State {
48
+ task: String::from("master"),
49
+ focus: String::new(),
50
+ message: String::new(),
51
+ include: vec![],
52
+ diff: vec![],
53
+ property: vec![],
54
+ save_update: None,
55
+ new_task: None,
56
+ filter: None,
57
+ dragged: None
58
+ }
59
+ }
60
+
61
+ pub fn reset(&mut self) {
62
+ self.message = String::new();
63
+ self.property = vec![];
64
+ self.include = vec![];
65
+ self.save_update = None;
66
+ self.new_task = None;
67
+ }
68
+
69
+ pub fn reset_with_status(&mut self) {
70
+ self.message = String::new();
71
+ self.property = self.property.iter().filter_map(|p| {
72
+ match &*p.name {
73
+ "Status" => Some(p.clone()),
74
+ "Ordinal" => Some(p.clone()),
75
+ _ => None
76
+ }
77
+ }).collect();
78
+ self.include = vec![];
79
+ self.save_update = None;
80
+ self.new_task = None;
81
+ }
82
+
83
+ pub fn apply(&mut self, mut last_state: Option<State>, rng: &mut rand::ThreadRng) -> Result<Option<State>, StateError> {
84
+ let new_last_state = self.clone();
85
+ let repo = Repository::open_from_env().unwrap_or(Repository::init(".").unwrap());
86
+ if let Some(ref mut last) = last_state {
87
+ println!("{}{:?}{}", color::Fg(color::LightBlack), last, color::Fg(color::Reset));
88
+ println!("{:?}", self);
89
+ println!("▶ ");
90
+ println!("Applying to repo: {:?}", repo.path());
91
+ if self.task != last.task && last.new_task.is_none() {
92
+ println!(" {}Task changing{} {} => {}", color::Fg(color::LightYellow), color::Fg(color::Reset), last.task, self.task);
93
+ if self.dragged.is_some() {
94
+ println!(" {}Saving last state due to dragging{}", color::Fg(color::LightGreen), color::Fg(color::Reset));
95
+ last.save_update(&repo, rng);
96
+ self.reset_with_status();
97
+ } else {
98
+ let switching_to_task = self.task.clone();
99
+ self.task = last.task.clone();
100
+ self.save_update(&repo, rng);
101
+ self.task = switching_to_task;
102
+ self.reset();
103
+ }
104
+ } else {
105
+ println!(" {}Updating task {}{}", color::Fg(color::LightGreen), self.task, color::Fg(color::Reset));
106
+ //self.apply_index(&repo);
107
+
108
+ if self.save_update.is_some() || self.new_task.is_some() {
109
+ self.save_update(&repo, rng);
110
+ }
111
+ }
112
+ } else {
113
+ if self.save_update.is_some() || self.new_task.is_some() {
114
+ self.save_update(&repo, rng);
115
+ }
116
+ }
117
+ Ok(Some(new_last_state))
118
+ }
119
+
120
+ fn save_update(&mut self, repo: &Repository, rng: &mut rand::ThreadRng) {
121
+ let mut index = repo.index().unwrap();
122
+ index.read(false).unwrap();
123
+
124
+ let author = repo.signature().unwrap();
125
+ let tree_oid = index.write_tree().unwrap();
126
+ let tree = repo.find_tree(tree_oid).unwrap();
127
+
128
+ let branch = repo.find_branch(&self.task, BranchType::Local);
129
+ match branch {
130
+ Ok(branch) => {
131
+ let head = branch.into_reference();
132
+ let commit = head.peel(ObjectType::Commit).unwrap();
133
+ let mut yaml = String::new();
134
+ let task = Task::from_ref(&head);
135
+ self.convert_to_yaml(&mut yaml, &repo, Some(task));
136
+ if self.message.len() > 0 || yaml.len() > 0 {
137
+ let message = format!("{}\n{}", self.message, yaml);
138
+ repo.commit(Some(&head.name().unwrap()), &author, &author, &message, &tree, &[&commit.as_commit().unwrap()]).unwrap();
139
+ }
140
+ }
141
+ Err(_) => {
142
+ println!(" {}Initial Commit{}", color::Fg(color::LightBlue), color::Fg(color::Reset));
143
+ let mut yaml = String::new();
144
+ self.convert_to_yaml(&mut yaml, &repo, None);
145
+ let message = format!("{}\n{}", self.message, yaml);
146
+ repo.commit(None, &author, &author, &message, &tree, &[]).unwrap();
147
+ }
148
+ };
149
+
150
+ println!(" {}Saved changes to {}{} {:?}", color::Fg(color::LightYellow), self.task, color::Fg(color::Reset), self);
151
+ if self.new_task.is_some() {
152
+ let num = rng.gen::<u16>();
153
+ let new_task = format!("{:X}", num);
154
+ println!(" {}Creating task {}{}", color::Fg(color::LightBlue), new_task, color::Fg(color::Reset));
155
+ if let Ok(master_branch) = repo.find_branch("master", BranchType::Local) {
156
+ let master = master_branch.into_reference();
157
+ let commit_obj = master.peel(ObjectType::Commit).unwrap();
158
+ let commit = commit_obj.as_commit().unwrap();
159
+ repo.branch(&new_task, &commit, false).unwrap();
160
+ self.task = new_task;
161
+ self.reset();
162
+ self.filter = Some(Filter {
163
+ name: String::from("Status"), value: String::from("Sprint")
164
+ });
165
+ println!(" State reset: {:?}", self);
166
+ } else {
167
+ panic!("Unable to find master branch from which to fork!");
168
+ }
169
+ }
170
+ }
171
+
172
+ // Constructing the properties YAML from State
173
+ fn convert_to_yaml(&self, mut yaml: &mut String, repo: &Repository, task: Option<Task>) {
174
+ println!(" Converting State to YAML");
175
+ let mut tasks = Hash::new();
176
+ let mut properties = Hash::new();
177
+ let mut emitter = YamlEmitter::new(&mut yaml);
178
+ for property in self.property.clone() {
179
+ let name = Yaml::String(property.name);
180
+ let new_value = match &*property.value {
181
+ "" => Yaml::Null,
182
+ _ => Yaml::String(property.value)
183
+ };
184
+ if let Some(ref task) = task {
185
+ if let Some(old_value) = task.get(&repo, &name) {
186
+ if old_value != new_value {
187
+ properties.insert(name, new_value);
188
+ }
189
+ } else {
190
+ properties.insert(name, new_value);
191
+ }
192
+ } else {
193
+ properties.insert(name, new_value);
194
+ }
195
+ }
196
+ if !properties.is_empty() {
197
+ tasks.insert(Yaml::String(self.task.clone()), Yaml::Hash(properties));
198
+ emitter.dump(&Yaml::Hash(tasks)).unwrap();
199
+ }
200
+ }
201
+
202
+ fn apply_index(&self, repo: &Repository) {
203
+ let branch = repo.find_branch(&self.task, BranchType::Local);
204
+ let head = match branch {
205
+ Ok(branch) => branch.into_reference(),
206
+ _ => match repo.head() {
207
+ Ok(head) => head,
208
+ _ => return // cannot apply index without a reference
209
+ }
210
+ };
211
+ let commit = head.peel(ObjectType::Commit).unwrap();
212
+ let mut index = repo.index().unwrap();
213
+
214
+ // apply git index changes only if task is the working directory
215
+ if self.task == repo.head().unwrap().shorthand().unwrap() {
216
+ let to_remove = index.iter().fold(vec![], |mut acc, entry| {
217
+ let entry_path = String::from_utf8_lossy(entry.path.as_ref());
218
+ match self.include.iter().find(|i| i.as_ref() == entry_path) {
219
+ None => acc.push(entry_path.into_owned()),
220
+ _ => {}
221
+ };
222
+ acc
223
+ });
224
+ repo.reset_default(Some(&commit), to_remove.iter()).unwrap();
225
+ for change in &self.include {
226
+ let path = Path::new(&change);
227
+ index.add_path(path).unwrap();
228
+ }
229
+ index.write().unwrap();
230
+ }
231
+ }
232
+
233
+ }