tinyci 0.4 → 0.5.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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