Compare commits

..

No commits in common. "d" and "master" have entirely different histories.
d ... master

22 changed files with 587 additions and 78 deletions

11
.gitignore vendored
View File

@ -1,3 +1,8 @@
/.rscons*
/build/
/svi
/.bundle/
/.yardoc
/_yardoc/
/coverage/
/doc/
/pkg/
/spec/reports/
/tmp/

4
Gemfile Normal file
View File

@ -0,0 +1,4 @@
source 'https://rubygems.org'
# Specify your gem's dependencies in svi.gemspec
gemspec

22
Gemfile.lock Normal file
View File

@ -0,0 +1,22 @@
PATH
remote: .
specs:
svi (0.1.0)
yawpa (~> 1.0)
GEM
remote: https://rubygems.org/
specs:
rake (10.4.2)
yawpa (1.3.0)
PLATFORMS
ruby
DEPENDENCIES
bundler (~> 1.10)
rake (~> 10.0)
svi!
BUNDLED WITH
1.10.6

21
LICENSE.txt Normal file
View File

@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (c) 2018 Josh Holtrop
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

41
README.md Normal file
View File

@ -0,0 +1,41 @@
# Svi
Welcome to your new gem! In this directory, you'll find the files you need to be able to package up your Ruby library into a gem. Put your Ruby code in the file `lib/svi`. To experiment with that code, run `bin/console` for an interactive prompt.
TODO: Delete this and the text above, and describe your gem
## Installation
Add this line to your application's Gemfile:
```ruby
gem 'svi'
```
And then execute:
$ bundle
Or install it yourself as:
$ gem install svi
## Usage
TODO: Write usage instructions here
## Development
After checking out the repo, run `bin/setup` to install dependencies. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
## Contributing
Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/svi.
## License
The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).

1
Rakefile Normal file
View File

@ -0,0 +1 @@
require "bundler/gem_tasks"

View File

@ -1,10 +0,0 @@
configure do
check_d_compiler
end
build do
Environment.new do |env|
env["D_IMPORT_PATH"] << "src"
env.Program("svi", glob("src/**/*.d"))
end
end

9
exe/svi Executable file
View File

@ -0,0 +1,9 @@
#!/usr/bin/env ruby
require "svi"
args = ARGV.dup
cli = Svi::Cli.new
exit(cli.run(args))

6
ext/svi/extconf.rb Normal file
View File

@ -0,0 +1,6 @@
require "mkmf"
abort("Error: missing curses.h") unless have_header("curses.h")
abort("Error: missing curses library") unless have_library("curses")
create_makefile "svi/svi"

30
ext/svi/svi.c Normal file
View File

@ -0,0 +1,30 @@
#include <ruby.h>
#include <curses.h>
static int Screen_Width = -1;
static int Screen_Height = -1;
VALUE Rb_screen_width(void)
{
return INT2FIX(Screen_Width);
}
VALUE Rb_screen_height(void)
{
return INT2FIX(Screen_Height);
}
void Init_svi(void)
{
/* Determine screen dimensions. */
initscr();
getmaxyx(stdscr, Screen_Height, Screen_Width);
endwin();
VALUE svi_module = rb_define_module("Svi");
VALUE c_module = rb_define_module_under(svi_module, "C");
rb_define_module_function(c_module, "screen_width",
Rb_screen_width, 0);
rb_define_module_function(c_module, "screen_height",
Rb_screen_height, 0);
}

8
lib/svi.rb Normal file
View File

@ -0,0 +1,8 @@
require_relative "svi/ansi"
require_relative "svi/application"
require_relative "svi/cli"
require_relative "svi/config"
require_relative "svi/svn_runner"
require_relative "svi/util"
require_relative "svi/version"
require "svi/svi"

21
lib/svi/ansi.rb Normal file
View File

@ -0,0 +1,21 @@
module Svi
module Ansi
class << self
def cursor_up(n = 1)
"\e[#{n}A"
end
def cursor_back(n = 1)
"\e[#{n}D"
end
def erase_cursor_to_eos
"\e[J"
end
end
end
end

105
lib/svi/application.rb Normal file
View File

@ -0,0 +1,105 @@
require "open3"
module Svi
class Application
def initialize
@svn_info = {}
@config = Config.new(self)
end
def wc_info
svn_info(".")
end
def checkout(url, options = {})
wc_path =
if options[:wc_path]
options[:wc_path]
elsif url =~ %r{/([^/]+)/trunk$}
$1
else
url.split("/").last
end
last_checkout_message = ""
checked_out_paths = []
clear_message = lambda do
if last_checkout_message.size > 0
clear = ""
lines = (last_checkout_message.size + C.screen_width - 1) / C.screen_width
if lines > 1
clear += Ansi.cursor_up(lines - 1)
end
clear += Ansi.cursor_back(999)
clear += Ansi.erase_cursor_to_eos
$stdout.write(clear)
last_checkout_message = ""
end
end
start_time = Time.new
SvnRunner.run_svn("checkout", [url, wc_path], allow_interactive: true) do |line|
if line =~ /^A.{4}(.*)$/
path = $1
checked_out_paths << path
clear_message[]
elapsed_time = Time.new - start_time
elapsed_time_formatted = Util.format_time(elapsed_time)
last_checkout_message = "Checking out #{path} [#{elapsed_time_formatted}]..."
$stdout.write(last_checkout_message)
$stdout.flush
elsif line =~ /^\sU\s{3}/
# Ignore the 'U'pdate line of the checkout directory itself.
elsif line =~ /^Checked out revision (\d+)/
revision = $1
clear_message[]
n_files = 0
n_directories = 0
checked_out_paths.uniq.each do |path|
if File.directory?(path)
n_directories += 1
else
n_files += 1
end
end
elapsed_time = Time.new - start_time
elapsed_time_formatted = Util.format_time(elapsed_time)
$stdout.puts "Checked out revision #{revision}: #{n_files} file#{n_files == 1 ? '' : 's'}, #{n_directories} director#{n_directories == 1 ? 'y' : 'ies'} [#{elapsed_time_formatted}]"
else
clear_message[]
$stdout.puts line
end
end
0
end
def status
SvnRunner.run_svn("status", []) do |line|
if line =~ %r{^([ACDIMRX\?!~ ])[CM ][L ][\+ ][SX ][KOTB ]..(.+)$}
action, path = $1, $2
if action == "?" and Util.is_path_ignored?(path, @config)
# Path is ignored
else
puts line
end
end
end
0
end
private
def svn_info(path)
@svn_info[path] ||= begin
info = {}
stdout, stderr, status = Open3.capture3("svn", "info", path)
stdout.lines.each do |line|
if line =~ /^(.*?):\s(.*)$/
info[$1] = $2
end
end
info
end
end
end
end

80
lib/svi/cli.rb Normal file
View File

@ -0,0 +1,80 @@
require "yawpa"
module Svi
class Cli
ALIASES = {
"co" => "checkout",
"st" => "status",
}
HELP_TEXT = <<EOS
Usage: svi [options] <command> [parameters...]
Global options:
--version show svi version and exit
--help, -h show this help and exit
Commands:
checkout/co check out Subversion URL
status/st show Subversion status
EOS
def initialize
@application = Application.new
end
def run(params)
options = {
version: {},
help: {short: "h"},
}
opts, args = Yawpa.parse(params, options, posix_order: true)
if opts[:version]
puts "svi, version #{Svi::VERSION}"
return 0
end
if opts[:help]
puts HELP_TEXT
return 0
end
if args.size < 1
$stderr.puts HELP_TEXT
return 1
end
run_subcommand(*args)
end
private
def run_subcommand(subcommand, *params)
if ALIASES[subcommand]
subcommand = ALIASES[subcommand]
end
command_function = "cmd_#{subcommand}".to_sym
if private_methods.include?(command_function)
__send__(command_function, params)
else
$stderr.puts "Unknown command #{subcommand}"
1
end
end
def cmd_checkout(params)
options = {}
opts, args = Yawpa.parse(params, options, posix_order: true)
if args.size < 1
$stderr.puts "Error: must specify URL"
return 1
end
url, wc_path = args
@application.checkout(url, wc_path: wc_path)
end
def cmd_status(params)
options = {}
opts, args = Yawpa.parse(params, options, posix_order: true)
@application.status
end
end
end

47
lib/svi/config.rb Normal file
View File

@ -0,0 +1,47 @@
module Svi
class Config
def initialize(application)
@application = application
@ignores = []
load_global_config
load_local_config
end
# Get the ignore patterns.
#
# @return [Array<String, Proc>]
# Each entry is a pattern or a Proc that returns a list of patterns.
def ignores
@_ignores ||= @ignores.map do |ignore|
if ignore.is_a?(Proc)
ignore[]
else
ignore
end
end.flatten
end
private
def load_global_config
global_config_path = "#{ENV["HOME"]}/.svi/config"
if File.exists?(global_config_path)
global_config = File.read(global_config_path)
instance_eval(global_config, global_config_path, 1)
end
end
def load_local_config
if wcrp = @application.wc_info["Working Copy Root Path"]
local_config_path = "#{wcrp}/.svn/svi/config"
if File.exists?(local_config_path)
local_config = File.read(local_config_path)
instance_eval(local_config, local_config_path, 1)
end
end
end
end
end

99
lib/svi/svn_runner.rb Normal file
View File

@ -0,0 +1,99 @@
module Svi
module SvnRunner
# Exception class to indicate an error with executing svn.
class SvnExecError < RuntimeError; end
class << self
# Run Subversion and yield results linewise.
#
# @param subcommand [String, nil]
# The svn subcommand to execute (e.g. "ls").
# @param args [Array<String>]
# The svn subcommand arguments.
# @param options [Hash]
# Optional arguments.
# @option options [Array<String>] :global_args
# Global svn arguments to place before the subcommand.
# @option options [Boolean] :allow_interactive
# Allow interaction with svn command.
#
# @yield [line]
# If a block is given, each line that the Subversion command writes to
# standard output will be yielded to the calling block.
# @yieldparam line [String]
# Line of standard output.
# @return [String]
# The standard output from svn.
# @raise [SvnExecError]
# If the svn command errors out this exception is raised.
def run_svn(subcommand, args, options = {}, &block)
options[:global_args] ||= []
options[:global_args] << "--non-interactive" unless options[:allow_interactive]
command = [
"svn",
*options[:global_args],
subcommand,
*args,
].compact
# Create pipes for standard output and standard error.
stdout_rd, stdout_wr = IO.pipe
stderr_rd, stderr_wr = IO.pipe
# Launch the svn subprocess using the pipes.
spawn_options = {
out: stdout_wr,
err: stderr_wr,
close_others: true,
}
spawn_options[:in] = :close unless options[:allow_interactive]
svn_pid = Process.spawn(*command, spawn_options)
# Close write side of the pipes in the parent process.
stdout_wr.close
stderr_wr.close
stdout = ""
stderr = ""
stdout_yield = ""
loop do
so = stdout_rd.read_nonblock(100000, exception: false)
if so.is_a?(String)
stdout += so
stdout_yield += so
end
se = stderr_rd.read_nonblock(100000, exception: false)
if se.is_a?(String)
stderr += se
end
if so.nil? and se.nil?
break
end
if block
loop do
index = stdout_yield.index("\n")
break unless index
line = stdout_yield[0, index]
block[line]
stdout_yield = stdout_yield[index + 1, stdout_yield.size]
end
end
IO.select([stdout_rd, stderr_rd])
end
Process.waitpid(svn_pid)
if stderr != ""
raise SvnExecError.new(stderr)
end
stdout
end
end
end
end

48
lib/svi/util.rb Normal file
View File

@ -0,0 +1,48 @@
module Svi
module Util
class << self
def is_path_ignored?(path, config)
config.ignores.find do |ignore_pattern|
ignore_pattern.chomp!("/")
if ignore_pattern.start_with?("/")
ignore_pattern = ignore_pattern[1, ignore_pattern.size]
elsif not ignore_pattern["/"]
path = File.basename(path)
end
File.fnmatch(ignore_pattern, path, File::FNM_PATHNAME)
end
end
def format_time(time)
if time < 10
sprintf("%.3fs", time)
else
days = (time / (60 * 60 * 24)).to_i
time -= days * (60 * 60 * 24)
hours = (time / (60 * 60)).to_i
time -= hours * (60 * 60)
minutes = (time / 60).to_i
time -= minutes * 60
incl = false
formatted = ""
if days != 0
incl = true
formatted += "#{days}d"
end
if hours != 0 or incl
incl = true
formatted += "#{hours}h"
end
if minutes != 0 or incl
formatted += "#{minutes}m"
end
formatted += "#{time.round}s"
end
end
end
end
end

3
lib/svi/version.rb Normal file
View File

@ -0,0 +1,3 @@
module Svi
VERSION = "0.1.0"
end

21
rscons

File diff suppressed because one or more lines are too long

View File

@ -1,8 +0,0 @@
import svn_runner;
import std.stdio;
int main(string[] args)
{
auto r = run_svn(["ls", "https://vcs.gentex.com/svn/embedded_sw"]);
return 0;
}

View File

@ -1,36 +0,0 @@
import std.process;
import std.stdio;
struct Result
{
int status;
char[] stdout;
char[] stderr;
}
private File nullfd()
{
return File("/dev/null", "r");
}
Result run_svn(string[] args)
{
string[] command = ["svn", "--non-interactive", "--no-auth-cache"] ~ args;
auto stdout_pipe = pipe();
auto stderr_pipe = pipe();
auto pid = spawnProcess(command, nullfd(), stdout_pipe.writeEnd,
stderr_pipe.writeEnd);
char[] stdout;
char[] stderr;
char[] buf;
while (stdout_pipe.readEnd.readln(buf))
{
stdout ~= buf;
}
while (stderr_pipe.readEnd.readln(buf))
{
stderr ~= buf;
}
int status = wait(pid);
return Result(status, stdout, stderr);
}

34
svi.gemspec Normal file
View File

@ -0,0 +1,34 @@
# coding: utf-8
lib = File.expand_path('../lib', __FILE__)
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
require 'svi/version'
Gem::Specification.new do |spec|
spec.name = "svi"
spec.version = Svi::VERSION
spec.authors = ["Josh Holtrop"]
spec.email = ["jholtrop@gmail.com"]
spec.summary = %q{Subversion Improved}
spec.description = %q{Subversion Improved.}
spec.homepage = "TODO: Put your gem's website or public repo URL here."
spec.license = "MIT"
# Prevent pushing this gem to RubyGems.org by setting 'allowed_push_host', or
# delete this section to allow pushing this gem to any host.
if spec.respond_to?(:metadata)
spec.metadata['allowed_push_host'] = "TODO: Set to 'http://mygemserver.com'"
else
raise "RubyGems 2.0 or newer is required to protect against public gem pushes."
end
spec.files = Dir['{bin,exe,ext,assets,integration,lib,spec,doc}/**/*', '*.gemspec', '.rspec']
spec.bindir = "exe"
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
spec.extensions = %w[ext/svi/extconf.rb]
spec.require_paths = ["lib"]
spec.add_dependency "yawpa", "~> 1.0"
spec.add_development_dependency "bundler", "~> 1.10"
spec.add_development_dependency "rake", "~> 10.0"
end