fun_ci 2.0.1 → 2.1.0
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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +101 -0
- data/README.md +33 -5
- data/lib/fun_ci/agent/command_support.rb +14 -1
- data/lib/fun_ci/agent/commands.rb +1 -0
- data/lib/fun_ci/agent/context.rb +3 -2
- data/lib/fun_ci/agent/events.rb +19 -5
- data/lib/fun_ci/agent/events_command.rb +1 -1
- data/lib/fun_ci/agent/exit_code.rb +1 -1
- data/lib/fun_ci/agent/next_step.rb +26 -0
- data/lib/fun_ci/agent/options.rb +6 -4
- data/lib/fun_ci/agent/output.rb +15 -2
- data/lib/fun_ci/agent/report_stage.rb +1 -1
- data/lib/fun_ci/agent/reports.rb +13 -9
- data/lib/fun_ci/agent/run_details.rb +31 -0
- data/lib/fun_ci/agent/run_report.rb +4 -3
- data/lib/fun_ci/agent/runs_text.rb +7 -4
- data/lib/fun_ci/agent/snapshots.rb +13 -3
- data/lib/fun_ci/agent/status_command.rb +2 -2
- data/lib/fun_ci/agent/status_json.rb +2 -1
- data/lib/fun_ci/agent/status_text.rb +13 -2
- data/lib/fun_ci/agent/trunk_json.rb +32 -0
- data/lib/fun_ci/agent/trunk_reading.rb +53 -0
- data/lib/fun_ci/agent/trunk_text.rb +50 -0
- data/lib/fun_ci/agent/trunk_verdict.rb +20 -0
- data/lib/fun_ci/agent/trunk_why.rb +52 -0
- data/lib/fun_ci/agent/trunk_why_json.rb +23 -0
- data/lib/fun_ci/agent/wait_command.rb +5 -5
- data/lib/fun_ci/agent/why_command.rb +11 -0
- data/lib/fun_ci/cli.rb +1 -1
- data/lib/fun_ci/cli_help.rb +4 -2
- data/lib/fun_ci/console/board_data.rb +22 -8
- data/lib/fun_ci/console/cancelled_folding.rb +19 -0
- data/lib/fun_ci/console/console_events.rb +5 -2
- data/lib/fun_ci/console/console_log.rb +4 -1
- data/lib/fun_ci/console/console_state.rb +7 -1
- data/lib/fun_ci/console/row_order.rb +31 -0
- data/lib/fun_ci/console/run_message.rb +9 -2
- data/lib/fun_ci/console/trunk_events.rb +43 -0
- data/lib/fun_ci/console/trunk_marks.rb +53 -0
- data/lib/fun_ci/console/view.rb +9 -8
- data/lib/fun_ci/evidence/caps.rb +7 -2
- data/lib/fun_ci/persistence/active_runs.rb +5 -4
- data/lib/fun_ci/persistence/database.rb +23 -2
- data/lib/fun_ci/persistence/null_recorder.rb +8 -0
- data/lib/fun_ci/persistence/pipeline_recorder.rb +3 -0
- data/lib/fun_ci/persistence/pipeline_run.rb +24 -3
- data/lib/fun_ci/persistence/trunk_checks.rb +68 -0
- data/lib/fun_ci/persistence/trunk_fetches.rb +57 -0
- data/lib/fun_ci/persistence/trunk_recording.rb +49 -0
- data/lib/fun_ci/pipeline/pipeline_forker.rb +17 -2
- data/lib/fun_ci/pipeline/process_runner.rb +18 -4
- data/lib/fun_ci/pipeline/trigger.rb +10 -1
- data/lib/fun_ci/pipeline/trigger_command.rb +9 -5
- data/lib/fun_ci/pipeline/trigger_params.rb +5 -3
- data/lib/fun_ci/pipeline/trunk_run.rb +81 -0
- data/lib/fun_ci/setup/agent_instructions.rb +3 -0
- data/lib/fun_ci/setup/gradle_linter_detector.rb +23 -0
- data/lib/fun_ci/setup/installer.rb +10 -10
- data/lib/fun_ci/setup/lint_detection.rb +32 -0
- data/lib/fun_ci/setup/maven_linter_detector.rb +4 -2
- data/lib/fun_ci/setup/project_config.rb +3 -1
- data/lib/fun_ci/setup/settings.rb +35 -3
- data/lib/fun_ci/setup/setup_checker.rb +7 -2
- data/lib/fun_ci/setup/stage_templates/managed.rb +12 -7
- data/lib/fun_ci/setup/stage_templates/native.rb +1 -1
- data/lib/fun_ci/setup/stage_templates/shell.rb +12 -3
- data/lib/fun_ci/setup/stage_templates.rb +1 -1
- data/lib/fun_ci/setup/trunk_report.rb +28 -0
- data/lib/fun_ci/trunk/check.rb +56 -0
- data/lib/fun_ci/trunk/checker.rb +91 -0
- data/lib/fun_ci/trunk/description.rb +52 -0
- data/lib/fun_ci/trunk/fetch.rb +58 -0
- data/lib/fun_ci/trunk/first_fetch.rb +27 -0
- data/lib/fun_ci/trunk/git.rb +102 -0
- data/lib/fun_ci/trunk/local.rb +58 -0
- data/lib/fun_ci/trunk/merge_check.rb +54 -0
- data/lib/fun_ci/trunk/regions.rb +38 -0
- data/lib/fun_ci/trunk/resolver.rb +64 -0
- data/lib/fun_ci/trunk/shown.rb +47 -0
- data/lib/fun_ci/trunk/tip_reader.rb +39 -0
- data/lib/fun_ci/version.rb +1 -1
- metadata +32 -1
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "shellwords"
|
|
4
|
+
require_relative "../pipeline/process_runner"
|
|
5
|
+
require_relative "resolver"
|
|
6
|
+
|
|
7
|
+
module FunCi
|
|
8
|
+
module Trunk
|
|
9
|
+
# How a fetch of the trunk went: nil error when it fetched.
|
|
10
|
+
Fetched = Data.define(:error)
|
|
11
|
+
|
|
12
|
+
# Fetches the trunk into a ref of fun-ci's own (architecture.md, Checking against the trunk,
|
|
13
|
+
# Keeping the trunk fresh). An empty refmap keeps git from moving the
|
|
14
|
+
# developer's remote-tracking ref too, and nothing may ask them anything:
|
|
15
|
+
# ssh runs in batch mode unless they have an ssh command of their own.
|
|
16
|
+
# Started in one thread, so its pid can be recorded, and finished in
|
|
17
|
+
# another, killed with its process group at the deadline.
|
|
18
|
+
class Fetch
|
|
19
|
+
QUIET = { "GIT_TERMINAL_PROMPT" => "0", "SSH_ASKPASS_REQUIRE" => "never", "GCM_INTERACTIVE" => "never" }.freeze
|
|
20
|
+
NO_PROMPT_SSH = 'if [ -z "$GIT_SSH_COMMAND" ] && ! git config --get core.sshCommand >/dev/null; then ' \
|
|
21
|
+
'export GIT_SSH_COMMAND="ssh -o BatchMode=yes -o ConnectTimeout=10"; fi; '
|
|
22
|
+
OPTIONS = %w[--quiet --no-tags --no-write-fetch-head --no-recurse-submodules --no-auto-maintenance
|
|
23
|
+
--refmap=].freeze
|
|
24
|
+
|
|
25
|
+
def self.ref_for(ref) = "refs/fun-ci/trunk/#{ref.remote}/#{ref.branch}"
|
|
26
|
+
|
|
27
|
+
# Why a fetch failed, from what git printed: its fatal line, else its first, else that it failed.
|
|
28
|
+
def self.reason(output)
|
|
29
|
+
lines = output.lines.map(&:strip).reject(&:empty?)
|
|
30
|
+
(lines.find { |line| line.start_with?("fatal:") } || lines.first || "git fetch failed").to_s
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# env: what the fetch finds in its environment besides fun-ci's own.
|
|
34
|
+
def initialize(dir, env:)
|
|
35
|
+
@launch = Pipeline::ProcessRunner::Launch.new(chdir: dir, env: QUIET.merge(env))
|
|
36
|
+
@runner = Object.new.extend(Pipeline::ProcessRunner)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Yields the fetch's pid before it runs.
|
|
40
|
+
def start(ref, &) = @runner.start_process(command(ref), @launch, &)
|
|
41
|
+
|
|
42
|
+
def finish(started, deadline:, timer: Pipeline::ProcessRunner::BUDGET)
|
|
43
|
+
output, status, over = @runner.await_process(started, deadline, launch: @launch, timer: timer)
|
|
44
|
+
return Fetched.new(error: "no answer within #{deadline} s") if over
|
|
45
|
+
|
|
46
|
+
Fetched.new(error: status.success? ? nil : self.class.reason(output))
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
private
|
|
50
|
+
|
|
51
|
+
def command(ref)
|
|
52
|
+
refspec = "+refs/heads/#{ref.branch}:#{self.class.ref_for(ref)}"
|
|
53
|
+
fetch = Shellwords.join(["git", "fetch", *OPTIONS, ref.remote, refspec])
|
|
54
|
+
"sh -c #{Shellwords.escape("#{NO_PROMPT_SSH}exec #{fetch}")}"
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../setup/project_config"
|
|
4
|
+
require_relative "../persistence/trunk_fetches"
|
|
5
|
+
require_relative "description"
|
|
6
|
+
require_relative "git"
|
|
7
|
+
require_relative "resolver"
|
|
8
|
+
|
|
9
|
+
module FunCi
|
|
10
|
+
module Trunk
|
|
11
|
+
# What to say before a project's first fetch of its trunk, where the
|
|
12
|
+
# developer sees it (architecture.md, Checking against the trunk):
|
|
13
|
+
# the post-commit hook's run is in the background, and what it prints is not.
|
|
14
|
+
module FirstFetch
|
|
15
|
+
# Nil unless the project fetches a remote trunk and never has yet; git lists
|
|
16
|
+
# the project's refs.
|
|
17
|
+
def self.notice(project, db, git: Git.new(project))
|
|
18
|
+
config = Setup::ProjectConfig.new(project)
|
|
19
|
+
interval = config.trunk_fetch
|
|
20
|
+
return nil if interval.nil? || config.trunk == "none" || Persistence::TrunkFetches.new(db, project).last
|
|
21
|
+
|
|
22
|
+
ref = Resolver.pick(config.trunk, git.refs)
|
|
23
|
+
ref&.remote ? Description.first_fetch(ref, interval) : nil
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "open3"
|
|
4
|
+
require "shellwords"
|
|
5
|
+
require_relative "../pipeline/process_runner"
|
|
6
|
+
require "tmpdir"
|
|
7
|
+
require_relative "../pipeline/git_environment"
|
|
8
|
+
require_relative "merge_check"
|
|
9
|
+
require_relative "resolver"
|
|
10
|
+
|
|
11
|
+
module FunCi
|
|
12
|
+
module Trunk
|
|
13
|
+
# The git the trunk check asks, in the project's repository. A merge
|
|
14
|
+
# writes its tree and conflicted blobs into a scratch object directory
|
|
15
|
+
# that borrows the repository's objects, so the repository keeps nothing.
|
|
16
|
+
class Git
|
|
17
|
+
REF_PREFIXES = %w[refs/heads/ refs/remotes/].freeze
|
|
18
|
+
|
|
19
|
+
# timer: whether a merge finished within its budget (ProcessRunner's timer), for tests to decide.
|
|
20
|
+
def initialize(dir, timer: Pipeline::ProcessRunner::BUDGET)
|
|
21
|
+
@dir = dir
|
|
22
|
+
@timer = timer
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def counts(commit, trunk) = run("rev-list", "--left-right", "--count", "#{commit}...#{trunk}")
|
|
26
|
+
|
|
27
|
+
def merge_tree(commit, trunk) = scratch { |env| merged(commit, trunk, env) }
|
|
28
|
+
|
|
29
|
+
# The merge's messages and each conflicted file as the merge leaves it.
|
|
30
|
+
def explain(commit, trunk)
|
|
31
|
+
scratch do |env|
|
|
32
|
+
output = MergeOutput.parse(merged(commit, trunk, env).out)
|
|
33
|
+
files = output.paths.to_h { |path| [path, run("cat-file", "-p", "#{output.tree}:#{path}", env: env).out] }
|
|
34
|
+
Explained.new(messages: output.messages, files: files)
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# The repository's top directory, or nil outside one.
|
|
39
|
+
def toplevel
|
|
40
|
+
answer = run("rev-parse", "--show-toplevel")
|
|
41
|
+
answer.status.zero? ? answer.out.strip : nil
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def version = run("--version").out[/\d+(?:\.\d+)+/]
|
|
45
|
+
|
|
46
|
+
def refs
|
|
47
|
+
remotes = run("remote").out.split
|
|
48
|
+
Refs.new(remotes: remotes, branches: branches, heads: remotes.to_h { |remote| [remote, head(remote)] }.compact)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# The commit a ref names, or nil when there is no such ref.
|
|
52
|
+
def rev(ref)
|
|
53
|
+
answer = run("rev-parse", "--verify", "--quiet", "#{ref}^{commit}")
|
|
54
|
+
answer.status.zero? ? answer.out.strip : nil
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# When the ref last moved, from its reflog, or else when its commit was made.
|
|
58
|
+
def moved_at(ref)
|
|
59
|
+
moved = run("reflog", "-1", "--date=unix", "--format=%gd", ref).out[/@\{(\d+)\}/, 1]
|
|
60
|
+
Time.at(Integer(moved || run("log", "-1", "--format=%ct", ref).out.strip))
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
private
|
|
64
|
+
|
|
65
|
+
def branches
|
|
66
|
+
names = run("for-each-ref", "--format=%(refname)", *REF_PREFIXES).out.split("\n")
|
|
67
|
+
names.reject { |name| name.end_with?("/HEAD") }.map { |name| strip_prefix(name) }
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def strip_prefix(name) = REF_PREFIXES.reduce(name) { |short, prefix| short.delete_prefix(prefix) }
|
|
71
|
+
|
|
72
|
+
def head(remote)
|
|
73
|
+
answer = run("symbolic-ref", "--quiet", "refs/remotes/#{remote}/HEAD")
|
|
74
|
+
answer.status.zero? ? answer.out.strip.delete_prefix("refs/remotes/#{remote}/") : nil
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# Within the check's budget, killed with its process group past it; what it
|
|
78
|
+
# prints to stderr comes with what it prints, which is all git says when it fails.
|
|
79
|
+
def merged(commit, trunk, env)
|
|
80
|
+
command = Shellwords.join(["git", "merge-tree", "--write-tree", "--name-only", "--messages", commit, trunk])
|
|
81
|
+
launch = Pipeline::ProcessRunner::Launch.new(chdir: @dir, env: env)
|
|
82
|
+
output, status, over = Object.new.extend(Pipeline::ProcessRunner)
|
|
83
|
+
.run_process_with_timeout(command, CHECK_BUDGET, launch: launch, timer: @timer)
|
|
84
|
+
over ? MergeCheck::OVER_BUDGET : MergeCheck::Answer.new(status: status.exitstatus, out: output, err: output)
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# Yields an environment whose new objects go to a scratch directory that is gone afterwards.
|
|
88
|
+
def scratch
|
|
89
|
+
Dir.mktmpdir("fun-ci-merge") do |dir|
|
|
90
|
+
yield({ "GIT_OBJECT_DIRECTORY" => dir, "GIT_ALTERNATE_OBJECT_DIRECTORIES" => objects })
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def objects = File.expand_path(File.join(run("rev-parse", "--git-common-dir").out.strip, "objects"), @dir)
|
|
95
|
+
|
|
96
|
+
def run(*, env: {})
|
|
97
|
+
out, err, status = Open3.capture3(Pipeline::GitEnvironment::CLEAN.merge(env), "git", *, chdir: @dir)
|
|
98
|
+
MergeCheck::Answer.new(status: status.exitstatus, out: out, err: err)
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
end
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../setup/project_config"
|
|
4
|
+
require_relative "checker"
|
|
5
|
+
require_relative "git"
|
|
6
|
+
require_relative "tip_reader"
|
|
7
|
+
|
|
8
|
+
module FunCi
|
|
9
|
+
module Trunk
|
|
10
|
+
# The trunk as the project's git has it now, for the agent commands
|
|
11
|
+
# (architecture.md, Checking against the trunk): where
|
|
12
|
+
# its ref points, read the way a check reads it, and a check against it
|
|
13
|
+
# that never fetches, since a command an agent runs must not wait on the network.
|
|
14
|
+
class Local
|
|
15
|
+
# Lets no fetch go, and knows the last one.
|
|
16
|
+
NoFetch = Data.define(:fetches)
|
|
17
|
+
|
|
18
|
+
# Reopened rather than given as a block to Data.define, so tools that
|
|
19
|
+
# read the source (mutineer) see these as NoFetch's methods.
|
|
20
|
+
class NoFetch
|
|
21
|
+
def claim(**) = nil
|
|
22
|
+
def last = fetches.last
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# dir: the project's directory, or one inside it.
|
|
26
|
+
def initialize(dir)
|
|
27
|
+
@dir = dir
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# The SHA the tip's trunk points at now, or nil when it is gone.
|
|
31
|
+
def now_at(tip)
|
|
32
|
+
ref = Ref.new(remote: tip.remote, branch: tip.branch)
|
|
33
|
+
TipReader.new(git, -> { Time.now }).tip(ref, fetching: !config.trunk_fetch.nil?)&.sha
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# fetches: the project's (Persistence::TrunkFetches), for when the last fetch was.
|
|
37
|
+
def check(sha, fetches)
|
|
38
|
+
checker = Checker.for(root)
|
|
39
|
+
pending = checker.start(sha, NoFetch.new(fetches: fetches))
|
|
40
|
+
pending && checker.finish(pending).check
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# The conflict between the commit and the tip it was checked against, merged
|
|
44
|
+
# again; nil when either is gone from the repository (a force push, then gc).
|
|
45
|
+
def explain(sha, tip)
|
|
46
|
+
return nil unless git.rev(sha) && git.rev(tip.sha)
|
|
47
|
+
|
|
48
|
+
git.explain(sha, tip.sha)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
private
|
|
52
|
+
|
|
53
|
+
def root = @root ||= Git.new(@dir).toplevel || @dir
|
|
54
|
+
def git = Git.new(root)
|
|
55
|
+
def config = Setup::ProjectConfig.new(root)
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "check"
|
|
4
|
+
|
|
5
|
+
module FunCi
|
|
6
|
+
module Trunk
|
|
7
|
+
# What merging a commit with a trunk tip would do (architecture.md, Checking against the trunk,
|
|
8
|
+
# How the check works), from what git answers: the commits each side has
|
|
9
|
+
# that the other lacks, then, when both have some, a merge in memory.
|
|
10
|
+
class MergeCheck
|
|
11
|
+
# What a git command answered: its exit status and what it printed.
|
|
12
|
+
Answer = Data.define(:status, :out, :err)
|
|
13
|
+
# A merge stopped at its budget, with its process group.
|
|
14
|
+
OVER_BUDGET = Answer.new(status: :over_budget, out: "", err: "")
|
|
15
|
+
STOPPED = "the merge ran over its #{CHECK_BUDGET} s budget; " \
|
|
16
|
+
"set trunk: none in .fun-ci/config to stop checking".freeze
|
|
17
|
+
|
|
18
|
+
def initialize(git)
|
|
19
|
+
@git = git
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# name: how the trunk is called in a reason, "origin/main".
|
|
23
|
+
def merge(commit, trunk, name)
|
|
24
|
+
counted = @git.counts(commit, trunk)
|
|
25
|
+
return Merge.unknown("git rev-list: #{first_line(counted)}") unless counted.status.zero?
|
|
26
|
+
|
|
27
|
+
ahead, behind = counted.out.split.map(&:to_i)
|
|
28
|
+
return Merge.clean(ahead: ahead, behind: behind) if ahead.zero? || behind.zero?
|
|
29
|
+
|
|
30
|
+
merged(@git.merge_tree(commit, trunk), name, ahead: ahead, behind: behind)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
private
|
|
34
|
+
|
|
35
|
+
def merged(answer, name, **counts)
|
|
36
|
+
return Merge.unknown(STOPPED) if answer.equal?(OVER_BUDGET)
|
|
37
|
+
return Merge.clean(**counts) if answer.status.zero?
|
|
38
|
+
return Merge.conflicts(MergeOutput.parse(answer.out).paths, **counts) if answer.status == 1
|
|
39
|
+
|
|
40
|
+
Merge.unknown(reason(answer, name))
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def reason(answer, name)
|
|
44
|
+
return "#{name} shares no history with this commit; set trunk: in .fun-ci/config" if unrelated?(answer)
|
|
45
|
+
return "needs git 2.38 or later; this is #{@git.version}" if answer.status == 129
|
|
46
|
+
|
|
47
|
+
"git merge-tree: #{first_line(answer)}"
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def unrelated?(answer) = answer.err.include?("unrelated histories")
|
|
51
|
+
def first_line(answer) = answer.err.lines.first.to_s.chomp
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module FunCi
|
|
4
|
+
module Trunk
|
|
5
|
+
# The conflicted regions of a file as a merge leaves it (architecture.md, Checking against the trunk,
|
|
6
|
+
# why REV trunk): each conflict from its <<<<<<< line to its >>>>>>> line,
|
|
7
|
+
# with `around` lines either side, regions that touch joined into one.
|
|
8
|
+
module Regions
|
|
9
|
+
# Lines first to last, as [number, text] pairs, numbered from 1.
|
|
10
|
+
Region = Data.define(:first, :last, :lines)
|
|
11
|
+
AROUND = 3
|
|
12
|
+
|
|
13
|
+
def self.of(text, around: AROUND)
|
|
14
|
+
lines = text.lines(chomp: true)
|
|
15
|
+
spans = conflicts(lines).map { |from, to| [[from - around, 0].max, [to + around, lines.size - 1].min] }
|
|
16
|
+
joined(spans).map { |from, to| region(lines, from, to) }
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def self.region(lines, from, to)
|
|
20
|
+
Region.new(first: from + 1, last: to + 1, lines: (from..to).map { |index| [index + 1, lines[index]] })
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# [index of <<<<<<<, index of >>>>>>>] for each conflict.
|
|
24
|
+
def self.conflicts(lines)
|
|
25
|
+
marked = ->(marker) { lines.each_index.select { |index| lines[index].start_with?(marker) } }
|
|
26
|
+
marked.call("<<<<<<<").zip(marked.call(">>>>>>>"))
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def self.joined(spans)
|
|
30
|
+
spans.each_with_object([]) do |(from, to), joined|
|
|
31
|
+
touching = joined.any? && from <= joined.last[1] + 1
|
|
32
|
+
touching ? joined[-1] = [joined.last[0], [joined.last[1], to].max] : joined << [from, to]
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
private_class_method :region, :conflicts, :joined
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module FunCi
|
|
4
|
+
module Trunk
|
|
5
|
+
# A branch that may be the trunk: on a remote, or local when remote is nil.
|
|
6
|
+
Ref = Data.define(:remote, :branch)
|
|
7
|
+
|
|
8
|
+
class Ref
|
|
9
|
+
def to_s = remote ? "#{remote}/#{branch}" : branch
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
# What a repository has to choose a trunk from: its remotes, its branches
|
|
13
|
+
# (a remote's as "origin/main", local ones by name) and each remote's
|
|
14
|
+
# default branch, where git knows it.
|
|
15
|
+
Refs = Data.define(:remotes, :branches, :heads)
|
|
16
|
+
|
|
17
|
+
# Which ref is the trunk (architecture.md, Checking against the trunk):
|
|
18
|
+
# the one configured, else the remote's default branch, else the first
|
|
19
|
+
# usual trunk name on the remote, else locally; nil when none exists.
|
|
20
|
+
module Resolver
|
|
21
|
+
USUAL = %w[main master develop development trunk].freeze
|
|
22
|
+
|
|
23
|
+
def self.pick(setting, refs)
|
|
24
|
+
return configured(setting, refs.remotes) if setting
|
|
25
|
+
|
|
26
|
+
remote = remote(refs.remotes)
|
|
27
|
+
remote ? on_remote(remote, refs) : on_local(refs.branches)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Why pick chose the trunk it did: :configured, :default (the remote's
|
|
31
|
+
# default branch), :usual (a usual trunk name on it) or :local.
|
|
32
|
+
def self.why(setting, refs)
|
|
33
|
+
return :configured if setting
|
|
34
|
+
|
|
35
|
+
remote = remote(refs.remotes)
|
|
36
|
+
return :local unless remote
|
|
37
|
+
|
|
38
|
+
refs.heads[remote] ? :default : :usual
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def self.configured(setting, remotes)
|
|
42
|
+
remote = remotes.find { |name| setting.start_with?("#{name}/") }
|
|
43
|
+
Ref.new(remote: remote, branch: remote ? setting.delete_prefix("#{remote}/") : setting)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def self.remote(remotes)
|
|
47
|
+
return "origin" if remotes.include?("origin")
|
|
48
|
+
|
|
49
|
+
remotes.first if remotes.size == 1
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def self.on_remote(remote, refs)
|
|
53
|
+
branch = refs.heads[remote] || USUAL.find { |name| refs.branches.include?("#{remote}/#{name}") }
|
|
54
|
+
branch && Ref.new(remote: remote, branch: branch)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def self.on_local(branches)
|
|
58
|
+
branch = USUAL.find { |name| branches.include?(name) }
|
|
59
|
+
branch && Ref.new(remote: nil, branch: branch)
|
|
60
|
+
end
|
|
61
|
+
private_class_method :configured, :remote, :on_remote, :on_local
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "check"
|
|
4
|
+
|
|
5
|
+
module FunCi
|
|
6
|
+
module Trunk
|
|
7
|
+
# A check as the developer and agents are shown it: its state, derived
|
|
8
|
+
# rather than stored, how many seconds ago its trunk tip was seen (nil
|
|
9
|
+
# when it had none), the project's last fetch (a LastFetch, nil if none), and
|
|
10
|
+
# the SHA the trunk has moved to since, if it has.
|
|
11
|
+
# A run whose check hasn't been recorded is checking until its fetch and
|
|
12
|
+
# merge are past their time, and then unknown.
|
|
13
|
+
Shown = Data.define(:state, :check, :age, :fetch, :moved_to)
|
|
14
|
+
|
|
15
|
+
class Shown
|
|
16
|
+
# How old a trunk may be before it is stale.
|
|
17
|
+
FRESH_FOR = 3600
|
|
18
|
+
|
|
19
|
+
def self.of(check, now:, fetch: nil, moved_to: nil)
|
|
20
|
+
new(state: state(check.merge), check: check, age: check.tip && (now - check.tip.seen_at),
|
|
21
|
+
fetch: fetch, moved_to: moved_to)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def self.unchecked(started:, now:)
|
|
25
|
+
if now - started <= FETCH_DEADLINE + CHECK_BUDGET
|
|
26
|
+
return new(state: "checking", check: nil, age: nil, fetch: nil, moved_to: nil)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
of(Check.new(commit: nil, tip: nil, merge: Merge.unknown("the check never finished")), now: now)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def self.state(merge)
|
|
33
|
+
return merge.outcome if merge.outcome == "unknown"
|
|
34
|
+
return "in_trunk" if merge.ahead.zero?
|
|
35
|
+
|
|
36
|
+
merge.behind.zero? ? "up_to_date" : merge.outcome
|
|
37
|
+
end
|
|
38
|
+
private_class_method :state
|
|
39
|
+
|
|
40
|
+
# Whether the trunk it was checked against may have moved on unseen.
|
|
41
|
+
def stale? = !fetch_error.nil? || (!age.nil? && age > FRESH_FOR)
|
|
42
|
+
|
|
43
|
+
# Why the project's last fetch failed, or nil.
|
|
44
|
+
def fetch_error = fetch&.error
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "check"
|
|
4
|
+
require_relative "fetch"
|
|
5
|
+
require_relative "resolver"
|
|
6
|
+
require_relative "../persistence/trunk_fetches"
|
|
7
|
+
|
|
8
|
+
module FunCi
|
|
9
|
+
module Trunk
|
|
10
|
+
# The tip of the trunk a check reads, and when it counts as seen
|
|
11
|
+
# (architecture.md, Checking against the trunk): fun-ci's own ref once
|
|
12
|
+
# it has fetched one, seen when it was last fetched; else the developer's
|
|
13
|
+
# ref, seen when it last moved.
|
|
14
|
+
class TipReader
|
|
15
|
+
# clock: answers the time now.
|
|
16
|
+
def initialize(git, clock)
|
|
17
|
+
@git = git
|
|
18
|
+
@clock = clock
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# fetching: whether fun-ci fetches this trunk; fetched: how this run's
|
|
22
|
+
# fetch went, if it made one; last: the fetch before it. Nil when the
|
|
23
|
+
# trunk doesn't exist.
|
|
24
|
+
def tip(ref, fetching:, fetched: nil, last: nil)
|
|
25
|
+
own = fetching && ref.remote && @git.rev(Fetch.ref_for(ref))
|
|
26
|
+
return at(ref, own, seen(fetched, last) || @git.moved_at(Fetch.ref_for(ref))) if own
|
|
27
|
+
|
|
28
|
+
full = ref.remote ? "refs/remotes/#{ref}" : "refs/heads/#{ref.branch}"
|
|
29
|
+
sha = @git.rev(full)
|
|
30
|
+
sha && at(ref, sha, @git.moved_at(full))
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
private
|
|
34
|
+
|
|
35
|
+
def seen(fetched, last) = fetched && !fetched.error ? @clock.call : last&.fetched_at
|
|
36
|
+
def at(ref, sha, seen_at) = Tip.new(remote: ref.remote, branch: ref.branch, sha: sha, seen_at: seen_at)
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
data/lib/fun_ci/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: fun_ci
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 2.0
|
|
4
|
+
version: 2.1.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Erik Thyge Madsen
|
|
@@ -73,10 +73,12 @@ files:
|
|
|
73
73
|
- lib/fun_ci/agent/exit_code.rb
|
|
74
74
|
- lib/fun_ci/agent/git.rb
|
|
75
75
|
- lib/fun_ci/agent/live_pipeline.rb
|
|
76
|
+
- lib/fun_ci/agent/next_step.rb
|
|
76
77
|
- lib/fun_ci/agent/options.rb
|
|
77
78
|
- lib/fun_ci/agent/output.rb
|
|
78
79
|
- lib/fun_ci/agent/report_stage.rb
|
|
79
80
|
- lib/fun_ci/agent/reports.rb
|
|
81
|
+
- lib/fun_ci/agent/run_details.rb
|
|
80
82
|
- lib/fun_ci/agent/run_report.rb
|
|
81
83
|
- lib/fun_ci/agent/runs_command.rb
|
|
82
84
|
- lib/fun_ci/agent/runs_text.rb
|
|
@@ -86,6 +88,12 @@ files:
|
|
|
86
88
|
- lib/fun_ci/agent/status_json.rb
|
|
87
89
|
- lib/fun_ci/agent/status_text.rb
|
|
88
90
|
- lib/fun_ci/agent/system_clock.rb
|
|
91
|
+
- lib/fun_ci/agent/trunk_json.rb
|
|
92
|
+
- lib/fun_ci/agent/trunk_reading.rb
|
|
93
|
+
- lib/fun_ci/agent/trunk_text.rb
|
|
94
|
+
- lib/fun_ci/agent/trunk_verdict.rb
|
|
95
|
+
- lib/fun_ci/agent/trunk_why.rb
|
|
96
|
+
- lib/fun_ci/agent/trunk_why_json.rb
|
|
89
97
|
- lib/fun_ci/agent/verdict.rb
|
|
90
98
|
- lib/fun_ci/agent/wait_command.rb
|
|
91
99
|
- lib/fun_ci/agent/waiting.rb
|
|
@@ -96,6 +104,7 @@ files:
|
|
|
96
104
|
- lib/fun_ci/cli_help.rb
|
|
97
105
|
- lib/fun_ci/console/board_data.rb
|
|
98
106
|
- lib/fun_ci/console/board_heartbeat.rb
|
|
107
|
+
- lib/fun_ci/console/cancelled_folding.rb
|
|
99
108
|
- lib/fun_ci/console/console_events.rb
|
|
100
109
|
- lib/fun_ci/console/console_log.rb
|
|
101
110
|
- lib/fun_ci/console/console_loop.rb
|
|
@@ -107,11 +116,14 @@ files:
|
|
|
107
116
|
- lib/fun_ci/console/milestones.rb
|
|
108
117
|
- lib/fun_ci/console/renderer_lookup.rb
|
|
109
118
|
- lib/fun_ci/console/renderer_process.rb
|
|
119
|
+
- lib/fun_ci/console/row_order.rb
|
|
110
120
|
- lib/fun_ci/console/run_message.rb
|
|
111
121
|
- lib/fun_ci/console/stage_change_detector.rb
|
|
112
122
|
- lib/fun_ci/console/stage_events.rb
|
|
113
123
|
- lib/fun_ci/console/stop_report.rb
|
|
114
124
|
- lib/fun_ci/console/streak_counter.rb
|
|
125
|
+
- lib/fun_ci/console/trunk_events.rb
|
|
126
|
+
- lib/fun_ci/console/trunk_marks.rb
|
|
115
127
|
- lib/fun_ci/console/view.rb
|
|
116
128
|
- lib/fun_ci/evidence/about.rb
|
|
117
129
|
- lib/fun_ci/evidence/about_stage.rb
|
|
@@ -205,6 +217,9 @@ files:
|
|
|
205
217
|
- lib/fun_ci/persistence/run_status.rb
|
|
206
218
|
- lib/fun_ci/persistence/stage_job.rb
|
|
207
219
|
- lib/fun_ci/persistence/state_dir.rb
|
|
220
|
+
- lib/fun_ci/persistence/trunk_checks.rb
|
|
221
|
+
- lib/fun_ci/persistence/trunk_fetches.rb
|
|
222
|
+
- lib/fun_ci/persistence/trunk_recording.rb
|
|
208
223
|
- lib/fun_ci/persistence/write_trouble.rb
|
|
209
224
|
- lib/fun_ci/pipeline/background_fork.rb
|
|
210
225
|
- lib/fun_ci/pipeline/command_executor.rb
|
|
@@ -230,6 +245,7 @@ files:
|
|
|
230
245
|
- lib/fun_ci/pipeline/trigger.rb
|
|
231
246
|
- lib/fun_ci/pipeline/trigger_command.rb
|
|
232
247
|
- lib/fun_ci/pipeline/trigger_params.rb
|
|
248
|
+
- lib/fun_ci/pipeline/trunk_run.rb
|
|
233
249
|
- lib/fun_ci/pipeline/workspaces.rb
|
|
234
250
|
- lib/fun_ci/pipeline/worktree_pool.rb
|
|
235
251
|
- lib/fun_ci/pipeline/worktree_prune.rb
|
|
@@ -237,10 +253,12 @@ files:
|
|
|
237
253
|
- lib/fun_ci/setup/agent_instructions.rb
|
|
238
254
|
- lib/fun_ci/setup/commands.rb
|
|
239
255
|
- lib/fun_ci/setup/git_hooks.rb
|
|
256
|
+
- lib/fun_ci/setup/gradle_linter_detector.rb
|
|
240
257
|
- lib/fun_ci/setup/hook_script.rb
|
|
241
258
|
- lib/fun_ci/setup/hook_writer.rb
|
|
242
259
|
- lib/fun_ci/setup/installer.rb
|
|
243
260
|
- lib/fun_ci/setup/legacy_hooks.rb
|
|
261
|
+
- lib/fun_ci/setup/lint_detection.rb
|
|
244
262
|
- lib/fun_ci/setup/maven_linter_detector.rb
|
|
245
263
|
- lib/fun_ci/setup/project_config.rb
|
|
246
264
|
- lib/fun_ci/setup/project_detector.rb
|
|
@@ -253,6 +271,19 @@ files:
|
|
|
253
271
|
- lib/fun_ci/setup/stage_templates/scripting.rb
|
|
254
272
|
- lib/fun_ci/setup/stage_templates/shell.rb
|
|
255
273
|
- lib/fun_ci/setup/template_writer.rb
|
|
274
|
+
- lib/fun_ci/setup/trunk_report.rb
|
|
275
|
+
- lib/fun_ci/trunk/check.rb
|
|
276
|
+
- lib/fun_ci/trunk/checker.rb
|
|
277
|
+
- lib/fun_ci/trunk/description.rb
|
|
278
|
+
- lib/fun_ci/trunk/fetch.rb
|
|
279
|
+
- lib/fun_ci/trunk/first_fetch.rb
|
|
280
|
+
- lib/fun_ci/trunk/git.rb
|
|
281
|
+
- lib/fun_ci/trunk/local.rb
|
|
282
|
+
- lib/fun_ci/trunk/merge_check.rb
|
|
283
|
+
- lib/fun_ci/trunk/regions.rb
|
|
284
|
+
- lib/fun_ci/trunk/resolver.rb
|
|
285
|
+
- lib/fun_ci/trunk/shown.rb
|
|
286
|
+
- lib/fun_ci/trunk/tip_reader.rb
|
|
256
287
|
- lib/fun_ci/version.rb
|
|
257
288
|
homepage: https://github.com/beatmadsen/fun-ci
|
|
258
289
|
licenses:
|