tinyci 0.4 → 0.5.1

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.
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'tinyci/subprocesses'
2
4
  require 'tinyci/logging'
3
5
  require 'ostruct'
@@ -5,33 +7,33 @@ require 'erb'
5
7
 
6
8
  module TinyCI
7
9
  # Parent class for Builder and Tester classes
8
- #
10
+ #
9
11
  # @abstract
10
12
  class Executor
11
13
  include Subprocesses
12
14
  include Logging
13
-
15
+
14
16
  # Returns a new instance of the executor.
15
- #
16
- # @param config [Hash] Configuration hash, typically taken from relevant key in the {Config} object.
17
- # @param logger [Logger] Logger object
18
- def initialize(config, logger: nil)
17
+ #
18
+ # @param config [Hash] Configuration hash, typically taken
19
+ # from relevant key in the {Config} object.
20
+ def initialize(config)
19
21
  @config = config
20
- @logger = logger
22
+ @logger = config[:logger]
21
23
  end
22
-
24
+
23
25
  def command
24
26
  ['/bin/sh', '-c', "'#{interpolate(@config[:command])}'"]
25
27
  end
26
-
28
+
27
29
  private
28
-
30
+
29
31
  def interpolate(command)
30
32
  erb = ERB.new command
31
-
33
+
32
34
  erb.result(erb_scope)
33
35
  end
34
-
36
+
35
37
  def template_vars
36
38
  OpenStruct.new(
37
39
  commit: @config[:commit],
@@ -1,16 +1,17 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'tinyci/subprocesses'
4
+ require 'git_clone_url'
2
5
 
3
6
  module TinyCI
4
-
5
7
  # Methods for dealing with git repos.
6
8
  module GitUtils
7
-
8
9
  # Returns the absolute path to the root of the current git directory
9
- #
10
+ #
10
11
  # @return [String] the path
11
12
  def repo_root
12
13
  return git_directory_path if inside_bare_repo?
13
-
14
+
14
15
  if inside_git_directory?
15
16
  File.expand_path('..', git_directory_path)
16
17
  elsif inside_work_tree?
@@ -19,48 +20,116 @@ module TinyCI
19
20
  raise 'not in git directory or work tree!?'
20
21
  end
21
22
  end
22
-
23
+
23
24
  # Are we currently under a git repo?
24
25
  def inside_git_directory?
25
26
  execute(git_cmd('rev-parse', '--is-inside-git-dir')) == 'true'
26
27
  end
27
-
28
+
28
29
  # Are we under a bare repo?
29
30
  def inside_bare_repo?
30
31
  execute(git_cmd('rev-parse', '--is-bare-repository')) == 'true'
31
32
  end
32
-
33
+
33
34
  # Are we currently under a git work tree?
34
35
  def inside_work_tree?
35
36
  execute(git_cmd('rev-parse', '--is-inside-work-tree')) == 'true'
36
37
  end
37
-
38
+
38
39
  # Are we currently under a repo in any sense?
39
40
  def inside_repository?
40
- execute(git_cmd('rev-parse', '--is-inside-work-tree', '--is-inside-git-dir')).split.any? {|s| s == 'true'}
41
+ cmd = git_cmd('rev-parse', '--is-inside-work-tree', '--is-inside-git-dir')
42
+ execute(cmd).split.any? { |s| s == 'true' }
41
43
  end
42
-
44
+
43
45
  # Returns the absolute path to the .git directory
44
46
  def git_directory_path
45
- File.expand_path(execute(git_cmd('rev-parse', '--git-dir')), defined?(@working_dir) ? @working_dir : nil)
47
+ base = defined?(@working_dir) ? @working_dir.to_s : nil
48
+
49
+ File.expand_path(execute(git_cmd('rev-parse', '--git-dir')), base)
50
+ end
51
+
52
+ # Does the specified file exist in the repo at the specified revision
53
+ def file_exists_in_git?(path, ref = @commit)
54
+ cmd = git_cmd('cat-file', '-e', "#{ref}:#{path}")
55
+
56
+ execute_and_return_status(cmd).success?
57
+ end
58
+
59
+ # Does the given sha exist in this repo's history?
60
+ def commit_exists?(commit = @commit)
61
+ cmd = git_cmd('cat-file', '-e', commit)
62
+
63
+ execute_and_return_status(cmd).success?
64
+ end
65
+
66
+ # Does the repo have a commit matching the given name?
67
+ def remote_exists?(remote = @remote)
68
+ execute(git_cmd('remote')).split("\n").include? remote
69
+ end
70
+
71
+ # Get the url for a given remote
72
+ # Alias for #push_url
73
+ def remote_url(remote = @remote)
74
+ push_url remote
46
75
  end
47
-
76
+
77
+ # Does the given remote point to github?
78
+ def github_remote?(remote = @remote)
79
+ remote_url(remote).host == 'github.com'
80
+ end
81
+
82
+ # Does the given remote point to an ssh url?
83
+ def ssh_remote?(remote = @remote)
84
+ remote_url(remote).is_a? URI::SshGit::Generic
85
+ end
86
+
87
+ # Return the upstream remote for the current branch
88
+ def current_tracking_remote
89
+ full = execute git_cmd 'rev-parse', '--symbolic-full-name', '--abbrev-ref', '@{push}'
90
+ full.split('/')[0]
91
+ rescue TinyCI::Subprocesses::SubprocessError => e
92
+ log_error 'Current branch does not have an upstream remote' if e.status.exitstatus == 128
93
+ end
94
+
95
+ # The current HEAD branch
96
+ def current_branch
97
+ execute git_cmd 'rev-parse', '--abbrev-ref', 'HEAD'
98
+ end
99
+
100
+ # The push url for the specified remote, parsed into a `URI` object
101
+ def push_url(remote = @remote)
102
+ url = raw_push_url(remote)
103
+ GitCloneUrl.parse url
104
+ rescue URI::InvalidComponentError
105
+ URI.parse url
106
+ end
107
+
108
+ # Parse the commit time from git
109
+ def time
110
+ @time ||= Time.at execute(git_cmd('show', '-s', '--format=%at', @commit)).to_i
111
+ end
112
+
48
113
  # Execute a git command, passing the -C parameter if the current object has
49
114
  # the working_directory instance var set
50
115
  def git_cmd(*args)
51
116
  cmd = ['git']
52
- cmd += ['-C', @working_dir] if defined?(@working_dir) && !@working_dir.nil?
117
+ cmd += ['-C', @working_dir.to_s] if defined?(@working_dir) && !@working_dir.nil?
53
118
  cmd += args
54
-
119
+
55
120
  cmd
56
121
  end
57
-
58
- private
59
-
122
+
123
+ # Get push url as a string. Not intended to be called directly, instead call {#push_url}
124
+ def raw_push_url(remote = @remote)
125
+ @push_urls ||= {}
126
+ @push_urls[remote] ||= execute git_cmd 'remote', 'get-url', '--push', remote
127
+ end
128
+
60
129
  def self.included(base)
61
130
  base.include Subprocesses
62
131
  end
63
-
132
+
64
133
  def self.extended(base)
65
134
  base.extend Subprocesses
66
135
  end
@@ -1,60 +1,59 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'tinyci/executor'
2
4
 
3
5
  module TinyCI
4
6
  module Hookers
5
7
  class ScriptHooker < TinyCI::Executor
6
8
  # All the hooks
7
- HOOKS = %w{
9
+ HOOKS = %w[
8
10
  before_build
9
-
11
+
10
12
  after_build_success
11
13
  after_build_failure
12
-
14
+
13
15
  after_build
14
-
16
+
15
17
  before_test
16
-
18
+
17
19
  after_test_success
18
20
  after_test_failure
19
-
21
+
20
22
  after_test
21
-
23
+
22
24
  after_all
23
- }
24
-
25
+ ].freeze
26
+
25
27
  # Those hooks that will halt exectution if they fail
26
- BLOCKING_HOOKS = %w{
28
+ BLOCKING_HOOKS = %w[
27
29
  before_build
28
30
  before_test
29
- }
30
-
31
+ ].freeze
32
+
31
33
  HOOKS.each do |hook|
32
- define_method hook+"_present?" do
34
+ define_method hook + '_present?' do
33
35
  @config.key? hook.to_sym
34
36
  end
35
-
36
- define_method hook+"!" do
37
+
38
+ define_method hook + '!' do
37
39
  return unless send("#{hook}_present?")
38
-
40
+
39
41
  log_info "executing #{hook} hook..."
40
42
  begin
41
43
  execute_stream(script_location(hook), label: hook, pwd: @config[:export])
42
-
44
+
43
45
  return true
44
46
  rescue SubprocessError => e
45
- if BLOCKING_HOOKS.include? hook
46
- raise e if ENV['TINYCI_ENV'] == 'test'
47
-
48
- log_error e
49
-
50
- return false
51
- else
52
- return true
53
- end
47
+ return true unless BLOCKING_HOOKS.include? hook
48
+
49
+ raise e if ENV['TINYCI_ENV'] == 'test'
50
+
51
+ log_error e
52
+ return false
54
53
  end
55
54
  end
56
55
  end
57
-
56
+
58
57
  def script_location(hook)
59
58
  ['/bin/sh', '-c', "'#{interpolate(@config[hook.to_sym])}'"]
60
59
  end
@@ -1,56 +1,63 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'fileutils'
2
4
  require 'tinyci/git_utils'
3
5
 
4
6
  module TinyCI
5
-
6
7
  # Responsible for writing the git hook file
7
8
  class Installer
8
9
  include GitUtils
9
-
10
+
10
11
  # Constructor
11
- #
12
- # @param [String] working_dir The directory from which to run. Does not have to be the root of the repo
12
+ #
13
+ # @param [String] working_dir The directory from which to run. Does not have to be the
14
+ # root of the repo.
13
15
  # @param [Logger] logger Logger object
14
- def initialize(working_dir: nil, logger: nil)
16
+ def initialize(working_dir: nil, logger: nil, absolute_path: false)
15
17
  @logger = logger
16
18
  @working_dir = working_dir || repo_root
19
+ @absolute_path = absolute_path
17
20
  end
18
-
21
+
19
22
  # Write the hook to the relevant path and make it executable
20
23
  def install!
21
24
  unless inside_repository?
22
- log_error "not currently in a git repository"
25
+ log_error 'not currently in a git repository'
23
26
  return false
24
27
  end
25
-
28
+
26
29
  if hook_exists?
27
- log_error "post-update hook already exists in this repository"
30
+ log_error 'post-update hook already exists in this repository'
28
31
  return false
29
32
  end
30
-
31
- File.open(hook_path, 'a') {|f| f.write hook_content}
33
+
34
+ File.open(hook_path, 'a') { |f| f.write hook_content }
32
35
  FileUtils.chmod('u+x', hook_path)
33
-
36
+
34
37
  log_info 'tinyci post-update hook installed successfully'
35
38
  end
36
-
39
+
37
40
  private
38
-
41
+
39
42
  def hook_exists?
40
43
  File.exist? hook_path
41
44
  end
42
-
45
+
43
46
  def hook_path
44
47
  File.expand_path('hooks/post-update', git_directory_path)
45
48
  end
46
-
49
+
50
+ def bin_path
51
+ @absolute_path ? Gem.bin_path('tinyci', 'tinyci') : 'tinyci'
52
+ end
53
+
47
54
  def hook_content
48
- <<-EOF
49
- #!/bin/sh
50
- unset GIT_DIR
55
+ <<~HOOK
56
+ #!/bin/sh
57
+ unset GIT_DIR
51
58
 
52
- #{Gem.bin_path('tinyci', 'tinyci')} run --all
53
- EOF
59
+ #{bin_path} run --all
60
+ HOOK
54
61
  end
55
62
  end
56
63
  end
@@ -0,0 +1,87 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'tinyci/path_utils'
4
+ require 'tinyci/git_utils'
5
+ require 'file-tail'
6
+
7
+ module TinyCI
8
+ # For reviewing the log files created by tinyCI runs. Can print lines from either a specific
9
+ # commit's logfile, or from the global logfile. Has functionality similar to the coreutils `tail`
10
+ # command.
11
+ class LogViewer
12
+ include PathUtils
13
+ include GitUtils
14
+
15
+ #
16
+ # Constructor
17
+ #
18
+ # @param [<Type>] working_dir The directory from which to run.
19
+ # @param [<Type>] commit The commit to run against
20
+ # @param [<Type>] follow After printing, instead of exiting, block and wait for additional data to be appended be the file and print it as it
21
+ # is written. Equivalent to unix `tail -f`
22
+ # @param [<Type>] num_lines How many lines of the file to print, starting from the end.
23
+ # Equivalent to unix `tail -n`
24
+ #
25
+ def initialize(working_dir:, commit: nil, follow: false, num_lines: nil)
26
+ @working_dir = working_dir
27
+ @commit = commit
28
+ @follow = follow
29
+ @num_lines = num_lines
30
+ end
31
+
32
+ def view!
33
+ if @follow
34
+ tail
35
+ else
36
+ dump
37
+ end
38
+ end
39
+
40
+ private
41
+
42
+ def dump
43
+ unless inside_repository?
44
+ warn 'Error: Not currently inside a git repo, or not on a branch'
45
+ return false
46
+ end
47
+
48
+ unless logfile_exists?
49
+ warn "Error: Logfile does not exist at #{logfile_to_read}"
50
+ warn "Did you mean \e[1mtinyci --remote #{current_tracking_remote} log\e[22m?"
51
+ return false
52
+ end
53
+
54
+ if @num_lines.nil?
55
+ puts File.read(logfile_to_read)
56
+ else
57
+ File.open(logfile_to_read) do |log|
58
+ log.extend File::Tail
59
+ log.return_if_eof = true
60
+ log.backward @num_lines if @num_lines
61
+ log.tail { |line| puts line }
62
+ end
63
+ end
64
+ end
65
+
66
+ def tail
67
+ File.open(logfile_to_read) do |log|
68
+ log.extend(File::Tail)
69
+
70
+ log.backward @num_lines if @num_lines
71
+ log.tail { |line| puts line }
72
+ end
73
+ end
74
+
75
+ def logfile_to_read
76
+ if @commit
77
+ logfile_path
78
+ else
79
+ repo_logfile_path
80
+ end
81
+ end
82
+
83
+ def logfile_exists?
84
+ File.exist? logfile_to_read
85
+ end
86
+ end
87
+ end
@@ -1,16 +1,16 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'tinyci/multi_logger'
2
4
 
3
5
  module TinyCI
4
6
  # Defines helper instance methods for logging to reduce code verbosity
5
7
  module Logging
6
-
7
- %w(log debug info warn error fatal unknown).each do |m|
8
+ %w[log debug info warn error fatal unknown].each do |m|
8
9
  define_method("log_#{m}") do |*args|
9
10
  return false unless defined?(@logger) && @logger.is_a?(MultiLogger)
10
-
11
+
11
12
  @logger.send(m, *args)
12
13
  end
13
- end
14
-
14
+ end
15
15
  end
16
16
  end
@@ -1,48 +1,57 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'logger'
4
+ require 'fileutils'
2
5
 
3
6
  module TinyCI
4
7
  # This class allows logging to both `STDOUT` and to a file with a single call.
5
8
  # @attr [Boolean] quiet Disables logging to STDOUT
6
9
  class MultiLogger
7
- FORMAT = Proc.new do |severity, datetime, progname, msg|
8
- "[#{datetime.strftime "%T"}] #{msg}\n"
10
+ FORMAT = proc do |_severity, datetime, _progname, msg|
11
+ "[#{datetime.strftime '%T'}] #{msg}\n"
9
12
  end
10
-
13
+
11
14
  LEVEL = Logger::INFO
12
-
15
+
13
16
  attr_accessor :quiet
14
-
17
+
15
18
  # Constructor
16
- #
19
+ #
17
20
  # @param [Boolean] quiet Disables logging to STDOUT
18
21
  # @param [String] path Location to write logfile to
19
- def initialize(quiet: false, path: nil)
20
- @file_logger = nil
21
- self.output_path = path
22
+ def initialize(quiet: false, path: nil, paths: [])
23
+ @file_loggers = []
24
+ add_output_path path
25
+ paths.each { |p| add_output_path(p) }
22
26
  @quiet = quiet
23
-
27
+
24
28
  @stdout_logger = Logger.new($stdout)
25
29
  @stdout_logger.formatter = FORMAT
26
30
  @stdout_logger.level = LEVEL
27
31
  end
28
-
32
+
29
33
  def targets
30
34
  logs = []
31
- logs << @file_logger if @file_logger
35
+ logs += @file_loggers
32
36
  logs << @stdout_logger unless @quiet
33
-
37
+
34
38
  logs
35
39
  end
36
-
37
- def output_path=(path)
38
- if path
39
- @file_logger = Logger.new(path)
40
- @file_logger.formatter = FORMAT
41
- @file_logger.level = LEVEL
42
- end
40
+
41
+ def add_output_path(path)
42
+ return unless path
43
+
44
+ FileUtils.touch path
45
+
46
+ logger = Logger.new(path)
47
+ logger.formatter = FORMAT
48
+ logger.level = LEVEL
49
+ @file_loggers << logger
50
+
51
+ logger
43
52
  end
44
53
 
45
- %w(log debug info warn error fatal unknown).each do |m|
54
+ %w[log debug info warn error fatal unknown].each do |m|
46
55
  define_method(m) do |*args|
47
56
  targets.each { |t| t.send(m, *args) }
48
57
  end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'tinyci/subprocesses'
4
+ require 'tinyci/git_utils'
5
+ require 'fileutils'
6
+
7
+ module TinyCI
8
+ # Methods for computing paths.
9
+ module PathUtils
10
+ def builds_path
11
+ File.absolute_path("#{@working_dir}/builds")
12
+ end
13
+
14
+ # Build the absolute target path
15
+ def target_path
16
+ File.join(builds_path, "#{time.to_i}_#{@commit}")
17
+ end
18
+
19
+ # Build the export path
20
+ def export_path
21
+ File.join(target_path, 'export')
22
+ end
23
+
24
+ private
25
+
26
+ def logfile_path
27
+ File.join(target_path, 'tinyci.log')
28
+ end
29
+
30
+ def repo_logfile_path
31
+ File.join(builds_path, 'tinyci.log')
32
+ end
33
+
34
+ # Ensure a path exists
35
+ def ensure_path(path)
36
+ FileUtils.mkdir_p path
37
+ end
38
+
39
+ def self.included(base)
40
+ base.include TinyCI::Subprocesses
41
+ base.include TinyCI::GitUtils
42
+ end
43
+ end
44
+ end