superconductor 0.0.4 → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|
+
}
|