| 1 | # |
|---|
| 2 | # reposh.rb - Reposh - Simple VCS Manager Shell |
|---|
| 3 | # |
|---|
| 4 | require 'readline' |
|---|
| 5 | require 'yaml' |
|---|
| 6 | require 'optparse' |
|---|
| 7 | |
|---|
| 8 | class Hash |
|---|
| 9 | def recursive_merge(other) |
|---|
| 10 | self.merge(other) do |key, my_val, other_val| |
|---|
| 11 | # for values of a same key |
|---|
| 12 | if my_val.is_a? Hash and other_val.is_a? Hash |
|---|
| 13 | my_val.recursive_merge(other_val) # XXX: hang-ups for cyclic hash? |
|---|
| 14 | else |
|---|
| 15 | other_val |
|---|
| 16 | end |
|---|
| 17 | end |
|---|
| 18 | end |
|---|
| 19 | end |
|---|
| 20 | |
|---|
| 21 | class Reposh |
|---|
| 22 | VERSION = "0.1.5" |
|---|
| 23 | CONF_DEFAULT = { |
|---|
| 24 | "global" => { |
|---|
| 25 | "editing_mode" => nil, |
|---|
| 26 | "custom_commands" => [], |
|---|
| 27 | "pathext" => [], |
|---|
| 28 | }, |
|---|
| 29 | "system" => { |
|---|
| 30 | "default" => { |
|---|
| 31 | "binpath" => nil, |
|---|
| 32 | "prompt" => "> ", |
|---|
| 33 | "default_cmd" => "status", |
|---|
| 34 | }, |
|---|
| 35 | "darcs" => { |
|---|
| 36 | "default_cmd" => "whatsnew --summary", |
|---|
| 37 | } |
|---|
| 38 | } |
|---|
| 39 | } |
|---|
| 40 | |
|---|
| 41 | def run |
|---|
| 42 | parse_option(ARGV) |
|---|
| 43 | @conf_path ||= File.join(ENV["HOME"], ".reposh.yaml") |
|---|
| 44 | @system_name ||= guess_system |
|---|
| 45 | |
|---|
| 46 | @conf = load_config(@conf_path) |
|---|
| 47 | @editing_mode = @conf["global"]["editing_mode"] |
|---|
| 48 | pathext = @conf["global"]["pathext"] |
|---|
| 49 | @prompt = get_conf(@system_name, "prompt") |
|---|
| 50 | binpath = get_conf(@system_name, "binpath") || @system_name |
|---|
| 51 | default_cmd = get_conf(@system_name, "default_cmd") |
|---|
| 52 | |
|---|
| 53 | @commands = Commands.new(binpath, default_cmd, pathext) |
|---|
| 54 | @commands.register_custom_commands(@conf["global"]["custom_commands"]) |
|---|
| 55 | |
|---|
| 56 | run_loop |
|---|
| 57 | end |
|---|
| 58 | |
|---|
| 59 | def parse_option(args) |
|---|
| 60 | o = OptionParser.new{|opt| |
|---|
| 61 | opt.on("-c confpath", |
|---|
| 62 | "path to .reposh.yaml"){|path| |
|---|
| 63 | @confpath = path |
|---|
| 64 | } |
|---|
| 65 | opt.on("-s system", |
|---|
| 66 | "vcs command name (eg. svn, svk, hg)"){|sys| |
|---|
| 67 | @system_name = sys |
|---|
| 68 | } |
|---|
| 69 | opt.on("-h", "--help", |
|---|
| 70 | "show this message"){ |
|---|
| 71 | puts opt |
|---|
| 72 | exit |
|---|
| 73 | } |
|---|
| 74 | opt.on("-v", "--version", |
|---|
| 75 | "show version information"){ |
|---|
| 76 | puts VERSION |
|---|
| 77 | exit |
|---|
| 78 | } |
|---|
| 79 | } |
|---|
| 80 | o.parse(args) |
|---|
| 81 | end |
|---|
| 82 | |
|---|
| 83 | def load_config(path) |
|---|
| 84 | if File.exist?(path) |
|---|
| 85 | config_hash = YAML.load(File.read(path)) |
|---|
| 86 | CONF_DEFAULT.recursive_merge(config_hash) |
|---|
| 87 | else |
|---|
| 88 | CONF_DEFAULT |
|---|
| 89 | end |
|---|
| 90 | end |
|---|
| 91 | |
|---|
| 92 | def guess_system |
|---|
| 93 | case |
|---|
| 94 | when File.directory?(".hg") |
|---|
| 95 | "hg" |
|---|
| 96 | when File.directory?("_darcs") |
|---|
| 97 | "darcs" |
|---|
| 98 | when File.directory?(".svn") |
|---|
| 99 | "svn" |
|---|
| 100 | else |
|---|
| 101 | "svk" |
|---|
| 102 | end |
|---|
| 103 | end |
|---|
| 104 | |
|---|
| 105 | def get_conf(system, prop) |
|---|
| 106 | (@conf["system"][system] and @conf["system"][system][prop]) or @conf["system"]["default"][prop] |
|---|
| 107 | end |
|---|
| 108 | |
|---|
| 109 | def run_loop |
|---|
| 110 | if @editing_mode == "vi" |
|---|
| 111 | Readline.vi_editing_mode |
|---|
| 112 | end |
|---|
| 113 | |
|---|
| 114 | puts "Welcome to reposh #{VERSION} (mode: #{@system_name})" |
|---|
| 115 | loop do |
|---|
| 116 | cmd = Readline.readline(@prompt, true) |
|---|
| 117 | @commands.dispatch(cmd, @system_name) |
|---|
| 118 | end |
|---|
| 119 | end |
|---|
| 120 | |
|---|
| 121 | class Commands |
|---|
| 122 | def initialize(binpath, default_cmd, pathext) |
|---|
| 123 | @binpath, @default_cmd, @pathext = binpath, default_cmd, pathext |
|---|
| 124 | @commands = [] |
|---|
| 125 | register_builtin_commands |
|---|
| 126 | end |
|---|
| 127 | |
|---|
| 128 | def register_builtin_commands |
|---|
| 129 | # default command |
|---|
| 130 | register(/.*/){|match| |
|---|
| 131 | cmd = (match[0] == "") ? @default_cmd : match[0] |
|---|
| 132 | execute "#{@binpath} #{cmd}" |
|---|
| 133 | } |
|---|
| 134 | |
|---|
| 135 | # system commands |
|---|
| 136 | register("%reload"){ |
|---|
| 137 | load __FILE__ |
|---|
| 138 | } |
|---|
| 139 | register("%env"){ |
|---|
| 140 | require 'pp' |
|---|
| 141 | pp ENV |
|---|
| 142 | } |
|---|
| 143 | register("%version"){ |
|---|
| 144 | puts VERSION |
|---|
| 145 | } |
|---|
| 146 | register(/\A%ruby (.*)/){|match| |
|---|
| 147 | puts "reposh: result is " + eval(match[1]).inspect |
|---|
| 148 | } |
|---|
| 149 | @trace_mode = false |
|---|
| 150 | register("%trace"){ |
|---|
| 151 | @trace_mode = (not @trace_mode) |
|---|
| 152 | puts "set trace_mode to #{@trace_mode}" |
|---|
| 153 | } |
|---|
| 154 | |
|---|
| 155 | # exit commands |
|---|
| 156 | exit_task = lambda{ |
|---|
| 157 | puts "" |
|---|
| 158 | exit |
|---|
| 159 | } |
|---|
| 160 | register(nil, &exit_task) |
|---|
| 161 | register("exit", &exit_task) |
|---|
| 162 | register("quit", &exit_task) |
|---|
| 163 | |
|---|
| 164 | # shell execution command |
|---|
| 165 | register(/^:(.*)/){|match| |
|---|
| 166 | execute match[1] |
|---|
| 167 | } |
|---|
| 168 | end |
|---|
| 169 | |
|---|
| 170 | def register_custom_commands(commands) |
|---|
| 171 | commands.each do |hash| |
|---|
| 172 | if hash["for"] |
|---|
| 173 | systems = hash["for"].split(/,/).map{|s| s.strip} |
|---|
| 174 | else |
|---|
| 175 | systems = nil |
|---|
| 176 | end |
|---|
| 177 | register(Regexp.new(hash["pattern"]), systems){|match| |
|---|
| 178 | cmd = hash["rule"]. |
|---|
| 179 | gsub(/\{system\}/, @binpath). |
|---|
| 180 | gsub(/\{\$(\d+)\}/){ match[$1.to_i] } |
|---|
| 181 | puts cmd |
|---|
| 182 | execute cmd |
|---|
| 183 | } |
|---|
| 184 | end |
|---|
| 185 | end |
|---|
| 186 | |
|---|
| 187 | def register(pattern, systems = nil, &task) |
|---|
| 188 | @commands.unshift [pattern, systems, task] |
|---|
| 189 | end |
|---|
| 190 | |
|---|
| 191 | def dispatch(cmd, sys) |
|---|
| 192 | @commands.each do |pattern, systems, task| |
|---|
| 193 | next if systems && !systems.include?(sys) |
|---|
| 194 | |
|---|
| 195 | if (match = match?(pattern, cmd)) |
|---|
| 196 | return task.call(match) |
|---|
| 197 | end |
|---|
| 198 | end |
|---|
| 199 | raise "must not happen" |
|---|
| 200 | end |
|---|
| 201 | |
|---|
| 202 | def match?(pat, value) |
|---|
| 203 | case pat |
|---|
| 204 | when Regexp |
|---|
| 205 | pat.match(value) |
|---|
| 206 | when nil |
|---|
| 207 | value == nil |
|---|
| 208 | else |
|---|
| 209 | pat.strip == value |
|---|
| 210 | end |
|---|
| 211 | end |
|---|
| 212 | |
|---|
| 213 | def execute(cmd) |
|---|
| 214 | stat = false |
|---|
| 215 | ([""] + @pathext).each do |ext| |
|---|
| 216 | command = add_ext(cmd, ext) |
|---|
| 217 | puts command if @trace_mode |
|---|
| 218 | result = system(command) |
|---|
| 219 | return if result |
|---|
| 220 | stat = $? |
|---|
| 221 | end |
|---|
| 222 | puts "reposh: failed to exec '#{cmd}': status #{stat.exitstatus}" |
|---|
| 223 | end |
|---|
| 224 | |
|---|
| 225 | def add_ext(cmd, ext) |
|---|
| 226 | exe, *args = cmd.split(' ') |
|---|
| 227 | "#{exe}#{ext} #{args.join ' '}" |
|---|
| 228 | end |
|---|
| 229 | |
|---|
| 230 | end |
|---|
| 231 | |
|---|
| 232 | end |
|---|
| 233 | |
|---|
| 234 | if $0==__FILE__ |
|---|
| 235 | Reposh.new.run |
|---|
| 236 | end |
|---|
| 237 | |
|---|