Compare commits

...

41 Commits

Author SHA1 Message Date
cc1393cb7a add st alias for status 2018-03-08 22:33:52 -05:00
68cd8c27cc update ignore logic to allow paths to start with /, end with /, and include / in the middle 2018-03-06 23:19:42 -05:00
96010f2590 checkout: accept wc_path optional argument 2018-03-06 22:56:28 -05:00
d96141f57d Show elapsed time 2018-02-27 21:21:28 -05:00
0330206e55 load a local config file 2018-02-27 21:04:33 -05:00
67beb1dece Hold a @config in Application 2018-02-27 20:42:41 -05:00
090daf39a0 Add Application class 2018-02-27 20:18:06 -05:00
6f55b3ddae Change Svi::Cli to a class 2018-02-27 19:52:28 -05:00
d9334aa224 add Config class and initial status handler 2018-02-24 22:28:10 -05:00
f1ed0cb50f checkout: specify checkout directory if URL ends with /trunk 2018-02-24 21:09:59 -05:00
8a795ce265 checkout: ignore 'U'pdate line, show number of files/directories checked out 2018-02-24 20:51:04 -05:00
0a1ddbfd3f add initial checkout handler 2018-02-24 20:36:39 -05:00
7af56e8363 get gem files from Dir[] instead of git ls-files 2018-02-24 20:36:17 -05:00
ee08d9d13e add C.screen_width and C.screen_height methods 2018-02-14 16:37:15 -05:00
66517921ab link with curses library 2018-02-14 11:54:45 -05:00
957f7ec0fd SvnRunner: add --non-interactive when not interactive 2018-02-12 21:35:38 -05:00
94c38ade8f SvnRunner.run_svn should be a class method 2018-02-12 21:22:26 -05:00
50529580c0 Begin parsing arguments and add checkout command handler 2018-02-12 21:11:32 -05:00
85157bc8f2 depend on yawpa 2018-02-12 20:45:55 -05:00
05ada6c504 extconf.rb: error out if curses.h is not found 2018-02-12 20:43:07 -05:00
7a820a99ea add C extension 2018-02-12 20:29:20 -05:00
a63adda1a8 gemspec: replace a couple TODO's 2018-02-12 20:20:08 -05:00
83075fd21d rename bin to exe; apparently that is where Ruby executables go now 2018-02-12 20:19:53 -05:00
5cedd30df6 add bin/svi and lib/svi/cli.rb 2018-02-12 20:18:12 -05:00
432bd7458e rearchitect as a gem
embedding Ruby in C did not go so well
2018-02-12 20:13:44 -05:00
9188bb42e2 add initial SvnRunner.run_svn() 2018-02-07 17:08:50 -05:00
c6be5a46a2 add yawpa 2018-02-06 22:07:43 -05:00
723b910fa3 set $ARGS global value 2018-02-06 21:50:16 -05:00
9f306cfbb5 add SvnRunner module 2018-02-06 21:42:02 -05:00
f91f98aae4 rename ruby_protect_eval_string() 2018-02-06 21:41:09 -05:00
c84fbddaca move ruby share files under share/svi/lib 2018-02-06 21:35:19 -05:00
ea585d0f5b set $SHARE_DIR 2018-02-06 21:31:20 -05:00
1ac247400d read bootstrap file relative to exe path 2018-02-06 21:15:17 -05:00
b7b587fb8f move Ruby bootstrapping to bootstrap.rb 2018-02-06 20:21:41 -05:00
7c5a2ebe3a print exception class 2018-01-29 17:15:28 -05:00
53ac0a2963 add initial share/svi/svi.rb 2018-01-29 17:11:24 -05:00
4f3fe36d90 add Ruby bootstrapping code 2018-01-29 17:01:14 -05:00
e82c661d50 Remove SvnRunner in preparation to use Ruby 2018-01-29 16:19:26 -05:00
e3c11407c3 install share files under ${PREFIX}/share/svi 2018-01-29 16:16:39 -05:00
8777e3f758 add several Makefile targets to redirect to waf 2018-01-29 16:13:36 -05:00
4a16e49672 require ruby development package 2018-01-25 14:10:33 -05:00
26 changed files with 587 additions and 327 deletions

11
.gitignore vendored
View File

@ -1,3 +1,8 @@
/.lock-waf* /.bundle/
/.waf* /.yardoc
/build/ /_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.

View File

@ -1,2 +0,0 @@
all:
./waf build

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"

2
configure vendored
View File

@ -1,2 +0,0 @@
#!/bin/sh
exec ./waf configure "$@"

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

View File

@ -1,82 +0,0 @@
#include "SvnRunner.h"
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>
SvnRunner::SvnRunner(std::vector<std::string> arguments)
{
m_pid = 0;
int fd[2];
int pipe_rc = pipe(fd);
if (pipe_rc == -1)
{
perror("pipe");
return;
}
m_pid = fork();
if (m_pid == -1)
{
perror("fork");
return;
}
if (m_pid == 0)
{
close(fd[0]);
int dup2_rc = dup2(fd[1], STDOUT_FILENO);
if (dup2_rc == -1)
{
perror("dup2");
return;
}
char const * argv[arguments.size() + 2];
argv[0] = "svn";
argv[arguments.size() + 1] = NULL;
for (unsigned int i = 0; i < arguments.size(); i++)
{
argv[i + 1] = arguments[i].c_str();
}
int exec_rc = execvp("/usr/bin/svn", (char **)argv);
if (exec_rc == -1)
{
perror("exec");
exit(1);
}
}
else
{
close(fd[1]);
m_n = 100;
m_line = (char *)malloc(m_n);
m_svn_fh = fdopen(fd[0], "r");
}
}
const char * SvnRunner::get_line()
{
if (m_pid > 0)
{
ssize_t getline_rc = getline(&m_line, &m_n, m_svn_fh);
if (getline_rc == -1)
{
fclose(m_svn_fh);
int status;
int waitpid_rc = waitpid(m_pid, &status, 0);
if (waitpid_rc == -1)
{
perror("waitpid");
}
m_status = WEXITSTATUS(status);
free(m_line);
m_pid = 0;
return NULL;
}
else
{
return m_line;
}
}
else
{
return NULL;
}
}

View File

@ -1,19 +0,0 @@
#include <string>
#include <vector>
#include <unistd.h>
#include <stdio.h>
class SvnRunner
{
public:
SvnRunner(std::vector<std::string> arguments);
const char * get_line();
int status() { return m_status; }
protected:
char * m_line;
size_t m_n;
pid_t m_pid;
FILE * m_svn_fh;
int m_status;
};

View File

@ -1,31 +0,0 @@
#include <iostream>
#include "SvnRunner.h"
#include <getopt.h>
#include "svi.h"
using namespace std;
enum
{
OPT_VERSION = 256,
};
int main(int argc, char * argv[])
{
static char const optstring[] = "+";
static struct option const longopts[] = {
{"version", no_argument, NULL, OPT_VERSION},
{NULL, 0, NULL, 0},
};
for (int o; (o = getopt_long(argc, argv, optstring, longopts, NULL)) != -1; )
{
switch (o)
{
case OPT_VERSION:
printf(APPLICATION_NAME " version " APPLICATION_VERSION "\n");
return 0;
}
}
return 0;
}

View File

@ -1,7 +0,0 @@
#ifndef SVI_H
#define SVI_H
#define APPLICATION_NAME "svi"
#define APPLICATION_VERSION "0.0.0alpha0"
#endif

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

169
waf vendored

File diff suppressed because one or more lines are too long

12
wscript
View File

@ -1,12 +0,0 @@
def options(opt):
opt.load("compiler_cxx")
def configure(conf):
conf.load("compiler_cxx")
conf.check(header_name = "getopt.h")
def build(bld):
bld(features = "cxx cxxprogram",
target = "svi",
source = bld.path.ant_glob("src/**/*.cc"),
cxxflags = ["-Wall", "-std=gnu++14"])