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/extconf.rb
ADDED
data/lib/superconductor.rb
CHANGED
@@ -1,4 +1,38 @@
|
|
1
|
-
require
|
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
|
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
data/src/state/state.rs
ADDED
@@ -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
|
+
}
|