ace-git 0.23.1 → 0.25.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/.ace-defaults/git/config.yml +21 -1
- data/CHANGELOG.md +67 -0
- data/docs/getting-started.md +4 -4
- data/docs/usage.md +53 -38
- data/lib/ace/git/atoms/command_executor.rb +1 -1
- data/lib/ace/git/atoms/pr_identifier.rb +66 -0
- data/lib/ace/git/atoms/pr_reference.rb +76 -0
- data/lib/ace/git/atoms/server_url.rb +56 -0
- data/lib/ace/git/atoms/status_formatter.rb +22 -27
- data/lib/ace/git/cli/commands/pr.rb +159 -84
- data/lib/ace/git/cli/commands/status.rb +16 -6
- data/lib/ace/git/cli.rb +8 -3
- data/lib/ace/git/errors.rb +82 -0
- data/lib/ace/git/models/diff_config.rb +2 -2
- data/lib/ace/git/models/repo_status.rb +1 -1
- data/lib/ace/git/molecules/branch_reader.rb +13 -7
- data/lib/ace/git/molecules/config_loader.rb +1 -1
- data/lib/ace/git/organisms/pull_request_lifecycle.rb +172 -0
- data/lib/ace/git/organisms/repo_status_loader.rb +72 -136
- data/lib/ace/git/providers/base.rb +221 -0
- data/lib/ace/git/providers/evidence.rb +96 -0
- data/lib/ace/git/providers.rb +121 -0
- data/lib/ace/git/resolved_server.rb +20 -0
- data/lib/ace/git/server_registry.rb +202 -0
- data/lib/ace/git/version.rb +1 -1
- data/lib/ace/git.rb +15 -14
- metadata +19 -13
- data/lib/ace/git/atoms/pr_identifier_parser.rb +0 -88
- data/lib/ace/git/molecules/gh_cli_executor.rb +0 -77
- data/lib/ace/git/molecules/github_issue_sync.rb +0 -248
- data/lib/ace/git/molecules/pr_metadata_fetcher.rb +0 -288
|
@@ -1,88 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Ace
|
|
4
|
-
module Git
|
|
5
|
-
module Atoms
|
|
6
|
-
# Parse PR identifiers into structured format
|
|
7
|
-
#
|
|
8
|
-
# Supports three formats:
|
|
9
|
-
# - Simple number: "123"
|
|
10
|
-
# - Qualified reference: "owner/repo#456"
|
|
11
|
-
# - GitHub URL: "https://github.com/owner/repo/pull/789"
|
|
12
|
-
#
|
|
13
|
-
# Consolidated from ace-bundle PrIdentifierParser
|
|
14
|
-
module PrIdentifierParser
|
|
15
|
-
# Parsed PR identifier result
|
|
16
|
-
ParseResult = Data.define(:number, :repo, :gh_format)
|
|
17
|
-
|
|
18
|
-
# Parse a PR identifier string
|
|
19
|
-
#
|
|
20
|
-
# Returns nil for nil/empty input (no PR specified), raises ArgumentError for
|
|
21
|
-
# invalid formats. This design allows callers to distinguish between "no PR"
|
|
22
|
-
# (nil input -> nil result) and "invalid PR" (bad format -> exception).
|
|
23
|
-
#
|
|
24
|
-
# @param input [String, Integer] PR identifier
|
|
25
|
-
# @return [ParseResult, nil] Parsed identifier with number, repo, and gh_format,
|
|
26
|
-
# or nil if input is nil/empty
|
|
27
|
-
# @raise [ArgumentError] if identifier format is invalid (non-empty but malformed)
|
|
28
|
-
#
|
|
29
|
-
# @example Simple number
|
|
30
|
-
# parse(123)
|
|
31
|
-
# # => ParseResult(number: "123", repo: nil, gh_format: "123")
|
|
32
|
-
#
|
|
33
|
-
# @example Qualified reference
|
|
34
|
-
# parse("owner/repo#456")
|
|
35
|
-
# # => ParseResult(number: "456", repo: "owner/repo", gh_format: "owner/repo#456")
|
|
36
|
-
#
|
|
37
|
-
# @example GitHub URL
|
|
38
|
-
# parse("https://github.com/owner/repo/pull/789")
|
|
39
|
-
# # => ParseResult(number: "789", repo: "owner/repo", gh_format: "owner/repo#789")
|
|
40
|
-
#
|
|
41
|
-
# @example Nil/empty input
|
|
42
|
-
# parse(nil) # => nil
|
|
43
|
-
# parse("") # => nil
|
|
44
|
-
# parse(" ") # => nil
|
|
45
|
-
# Maximum length for PR identifier to prevent ReDoS attacks
|
|
46
|
-
# GitHub URLs are typically under 200 chars, this provides generous margin
|
|
47
|
-
MAX_IDENTIFIER_LENGTH = 256
|
|
48
|
-
|
|
49
|
-
def self.parse(input)
|
|
50
|
-
return nil if input.nil?
|
|
51
|
-
|
|
52
|
-
input_str = input.to_s.strip
|
|
53
|
-
return nil if input_str.empty?
|
|
54
|
-
|
|
55
|
-
# Validate length to prevent ReDoS attacks on regex patterns
|
|
56
|
-
if input_str.length > MAX_IDENTIFIER_LENGTH
|
|
57
|
-
raise ArgumentError, "PR identifier too long (max #{MAX_IDENTIFIER_LENGTH} characters)"
|
|
58
|
-
end
|
|
59
|
-
|
|
60
|
-
case input_str
|
|
61
|
-
when /^(\d+)$/
|
|
62
|
-
# Simple PR number: "123"
|
|
63
|
-
number = ::Regexp.last_match(1)
|
|
64
|
-
# Reject zero: GitHub PR numbers are positive integers starting from 1
|
|
65
|
-
raise ArgumentError, "Invalid PR identifier format: #{input_str}" if number.to_i.zero?
|
|
66
|
-
# Normalize to canonical form (strip leading zeros) for consistent gh_format
|
|
67
|
-
canonical_number = number.to_i.to_s
|
|
68
|
-
ParseResult.new(number: canonical_number, repo: nil, gh_format: canonical_number)
|
|
69
|
-
|
|
70
|
-
when /^(?<repo>[a-zA-Z0-9_\-.]+\/[a-zA-Z0-9_\-.]+)#(?<number>\d+)$/
|
|
71
|
-
# Qualified reference: "owner/repo#456"
|
|
72
|
-
# GitHub owner/repo names: alphanumeric, hyphens, underscores, dots only
|
|
73
|
-
match = ::Regexp.last_match
|
|
74
|
-
ParseResult.new(number: match[:number], repo: match[:repo], gh_format: "#{match[:repo]}##{match[:number]}")
|
|
75
|
-
|
|
76
|
-
when %r{github\.com/(?<repo>[^/]+/[^/]+)/pull/(?<number>\d+)}
|
|
77
|
-
# GitHub URL: "https://github.com/owner/repo/pull/789"
|
|
78
|
-
match = ::Regexp.last_match
|
|
79
|
-
ParseResult.new(number: match[:number], repo: match[:repo], gh_format: "#{match[:repo]}##{match[:number]}")
|
|
80
|
-
|
|
81
|
-
else
|
|
82
|
-
raise ArgumentError, "Invalid PR identifier format: #{input_str}"
|
|
83
|
-
end
|
|
84
|
-
end
|
|
85
|
-
end
|
|
86
|
-
end
|
|
87
|
-
end
|
|
88
|
-
end
|
|
@@ -1,77 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require "open3"
|
|
4
|
-
require "timeout"
|
|
5
|
-
|
|
6
|
-
module Ace
|
|
7
|
-
module Git
|
|
8
|
-
module Molecules
|
|
9
|
-
# Safely execute gh CLI commands with timeout and structured output.
|
|
10
|
-
class GhCliExecutor
|
|
11
|
-
DEFAULT_GH_TIMEOUT = 30
|
|
12
|
-
DEFAULT_SIMPLE_TIMEOUT = 10
|
|
13
|
-
|
|
14
|
-
def self.execute(subcommand, args = [], timeout: nil)
|
|
15
|
-
check_installed
|
|
16
|
-
timeout_seconds = timeout || Ace::Git.network_timeout || DEFAULT_GH_TIMEOUT
|
|
17
|
-
command = ["gh", subcommand] + args
|
|
18
|
-
run_command(command, timeout_seconds)
|
|
19
|
-
rescue Timeout::Error
|
|
20
|
-
raise Ace::Git::TimeoutError, "gh command timed out after #{timeout_seconds} seconds"
|
|
21
|
-
end
|
|
22
|
-
|
|
23
|
-
def self.check_installed
|
|
24
|
-
result = execute_simple("--version")
|
|
25
|
-
raise Ace::Git::GhNotInstalledError unless result[:success]
|
|
26
|
-
|
|
27
|
-
true
|
|
28
|
-
end
|
|
29
|
-
|
|
30
|
-
def self.check_authenticated
|
|
31
|
-
result = execute_simple("auth", ["status"])
|
|
32
|
-
raise Ace::Git::GhAuthenticationError unless result[:success]
|
|
33
|
-
|
|
34
|
-
{
|
|
35
|
-
authenticated: true,
|
|
36
|
-
username: extract_username(result[:stderr])
|
|
37
|
-
}
|
|
38
|
-
end
|
|
39
|
-
|
|
40
|
-
def self.execute_simple(command, args = [], timeout_seconds = nil)
|
|
41
|
-
timeout_seconds ||= Ace::Git.network_timeout || DEFAULT_SIMPLE_TIMEOUT
|
|
42
|
-
cmd = ["gh", command] + args
|
|
43
|
-
run_command(cmd, timeout_seconds)
|
|
44
|
-
rescue Timeout::Error
|
|
45
|
-
{
|
|
46
|
-
success: false,
|
|
47
|
-
stdout: "",
|
|
48
|
-
stderr: "Command timed out",
|
|
49
|
-
exit_code: 1
|
|
50
|
-
}
|
|
51
|
-
end
|
|
52
|
-
|
|
53
|
-
def self.extract_username(output)
|
|
54
|
-
match = output.match(/Logged in to .+ as (\S+)/)
|
|
55
|
-
match ? match[1] : nil
|
|
56
|
-
end
|
|
57
|
-
|
|
58
|
-
def self.run_command(command, timeout_seconds)
|
|
59
|
-
stdout_str, stderr_str, status = Timeout.timeout(timeout_seconds) do
|
|
60
|
-
Open3.capture3(*command)
|
|
61
|
-
end
|
|
62
|
-
|
|
63
|
-
{
|
|
64
|
-
success: status.success?,
|
|
65
|
-
stdout: stdout_str,
|
|
66
|
-
stderr: stderr_str,
|
|
67
|
-
exit_code: status.exitstatus
|
|
68
|
-
}
|
|
69
|
-
rescue Errno::ENOENT
|
|
70
|
-
raise Ace::Git::GhNotInstalledError
|
|
71
|
-
end
|
|
72
|
-
|
|
73
|
-
private_class_method :execute_simple, :extract_username, :run_command
|
|
74
|
-
end
|
|
75
|
-
end
|
|
76
|
-
end
|
|
77
|
-
end
|
|
@@ -1,248 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require "json"
|
|
4
|
-
require "open3"
|
|
5
|
-
|
|
6
|
-
module Ace
|
|
7
|
-
module Git
|
|
8
|
-
module Molecules
|
|
9
|
-
# Synchronize ACE task linkage metadata to GitHub issues.
|
|
10
|
-
class GithubIssueSync
|
|
11
|
-
class OwnershipConflict < StandardError; end
|
|
12
|
-
|
|
13
|
-
STICKY_MARKER = "<!-- ace-task:tracked -->"
|
|
14
|
-
TRACKED_LABEL = "ace:tracked"
|
|
15
|
-
TERMINAL_STATUSES = %w[done completed shipped closed cancelled skipped archived].freeze
|
|
16
|
-
|
|
17
|
-
def self.sync_task(task_id:, task_title:, task_status:, task_path:, issue_ids:, reason:, previous: nil,
|
|
18
|
-
current_issue_ids: nil)
|
|
19
|
-
current_ids = if current_issue_ids.nil?
|
|
20
|
-
Array(issue_ids).map(&:to_i).uniq
|
|
21
|
-
else
|
|
22
|
-
Array(current_issue_ids).map(&:to_i).uniq
|
|
23
|
-
end
|
|
24
|
-
|
|
25
|
-
issue_ids.each do |issue_id|
|
|
26
|
-
sync_issue(
|
|
27
|
-
issue_id: issue_id.to_i,
|
|
28
|
-
task_id: task_id,
|
|
29
|
-
task_title: task_title,
|
|
30
|
-
task_status: task_status,
|
|
31
|
-
task_path: task_path,
|
|
32
|
-
reason: reason,
|
|
33
|
-
previous: previous,
|
|
34
|
-
currently_linked: current_ids.include?(issue_id.to_i)
|
|
35
|
-
)
|
|
36
|
-
end
|
|
37
|
-
|
|
38
|
-
{success: true, synced: issue_ids.length, issues: issue_ids}
|
|
39
|
-
end
|
|
40
|
-
|
|
41
|
-
def self.sync_issue(issue_id:, task_id:, task_title:, task_status:, task_path:, reason:, previous:,
|
|
42
|
-
currently_linked:)
|
|
43
|
-
issue = fetch_issue(issue_id)
|
|
44
|
-
sticky = find_sticky_comment(issue["comments"] || [])
|
|
45
|
-
|
|
46
|
-
lines = tracked_lines(sticky&.dig("body"))
|
|
47
|
-
validate_link!(issue_id: issue_id, task_id: task_id, previous_task_id: previous&.[](:id), lines: lines) if currently_linked
|
|
48
|
-
lines = remove_previous_line(lines, previous)
|
|
49
|
-
lines = upsert_line(lines, task_id: task_id, task_title: task_title, task_path: task_path) if currently_linked
|
|
50
|
-
if lines.empty?
|
|
51
|
-
cleanup_tracking_artifacts(issue_id: issue_id, sticky: sticky, labels: issue["labels"] || [])
|
|
52
|
-
return {success: true, issue: issue_id, reason: reason}
|
|
53
|
-
end
|
|
54
|
-
|
|
55
|
-
body = render_body(lines)
|
|
56
|
-
|
|
57
|
-
if sticky
|
|
58
|
-
update_comment(issue_id: issue_id, comment: sticky, body: body)
|
|
59
|
-
else
|
|
60
|
-
create_comment(issue_id: issue_id, body: body)
|
|
61
|
-
end
|
|
62
|
-
|
|
63
|
-
ensure_label(issue_id, issue["labels"] || [])
|
|
64
|
-
if currently_linked
|
|
65
|
-
sync_lifecycle(
|
|
66
|
-
issue_id: issue_id,
|
|
67
|
-
issue_state: issue["state"],
|
|
68
|
-
task_status: task_status
|
|
69
|
-
)
|
|
70
|
-
end
|
|
71
|
-
|
|
72
|
-
{success: true, issue: issue_id, reason: reason}
|
|
73
|
-
end
|
|
74
|
-
|
|
75
|
-
def self.cleanup_tracking_artifacts(issue_id:, sticky:, labels:)
|
|
76
|
-
delete_comment(issue_id: issue_id, comment: sticky) if sticky
|
|
77
|
-
remove_label(issue_id, labels)
|
|
78
|
-
end
|
|
79
|
-
|
|
80
|
-
def self.fetch_issue(issue_id)
|
|
81
|
-
result = GhCliExecutor.execute("issue", ["view", issue_id.to_s, "--json", "state,comments,labels"])
|
|
82
|
-
raise "Failed to fetch issue #{issue_id}: #{result[:stderr]}" unless result[:success]
|
|
83
|
-
|
|
84
|
-
JSON.parse(result[:stdout])
|
|
85
|
-
rescue JSON::ParserError => e
|
|
86
|
-
raise "Failed to parse issue #{issue_id} response: #{e.message}"
|
|
87
|
-
end
|
|
88
|
-
|
|
89
|
-
def self.find_sticky_comment(comments)
|
|
90
|
-
comments.find { |comment| comment["body"].to_s.include?(STICKY_MARKER) }
|
|
91
|
-
end
|
|
92
|
-
|
|
93
|
-
def self.tracked_lines(body)
|
|
94
|
-
body.to_s.lines.map(&:rstrip).select { |line| line.start_with?("Tracked in ace-task: ") }
|
|
95
|
-
end
|
|
96
|
-
|
|
97
|
-
def self.remove_previous_line(lines, previous)
|
|
98
|
-
return lines unless previous.is_a?(Hash)
|
|
99
|
-
|
|
100
|
-
previous_id = previous[:id] || previous["id"]
|
|
101
|
-
return lines unless previous_id
|
|
102
|
-
|
|
103
|
-
lines.reject { |line| extract_task_id(line) == previous_id.to_s }
|
|
104
|
-
end
|
|
105
|
-
|
|
106
|
-
def self.upsert_line(lines, task_id:, task_title:, task_path:)
|
|
107
|
-
filtered = lines.reject { |line| extract_task_id(line) == task_id.to_s }
|
|
108
|
-
filtered << tracked_line(task_id: task_id, task_title: task_title, task_path: task_path)
|
|
109
|
-
filtered.uniq.sort
|
|
110
|
-
end
|
|
111
|
-
|
|
112
|
-
def self.tracked_line(task_id:, task_title:, task_path:)
|
|
113
|
-
"Tracked in ace-task: [#{task_id}](#{task_url(task_path)})"
|
|
114
|
-
end
|
|
115
|
-
|
|
116
|
-
def self.extract_task_id(line)
|
|
117
|
-
match = line.match(/\[([^\]]+)\]\(/)
|
|
118
|
-
match ? match[1] : nil
|
|
119
|
-
end
|
|
120
|
-
|
|
121
|
-
def self.render_body(lines)
|
|
122
|
-
([STICKY_MARKER] + lines).join("\n")
|
|
123
|
-
end
|
|
124
|
-
|
|
125
|
-
def self.create_comment(issue_id:, body:)
|
|
126
|
-
GhCliExecutor.execute("issue", ["comment", issue_id.to_s, "--body", body]).tap do |result|
|
|
127
|
-
raise "Failed to create sticky comment for issue #{issue_id}: #{result[:stderr]}" unless result[:success]
|
|
128
|
-
end
|
|
129
|
-
end
|
|
130
|
-
|
|
131
|
-
def self.update_comment(issue_id:, comment:, body:)
|
|
132
|
-
comment_id = comment_api_id(comment)
|
|
133
|
-
GhCliExecutor.execute("api", [
|
|
134
|
-
"repos/{owner}/{repo}/issues/comments/#{comment_id}",
|
|
135
|
-
"--method", "PATCH",
|
|
136
|
-
"--field", "body=#{body}"
|
|
137
|
-
]).tap do |result|
|
|
138
|
-
raise "Failed to update sticky comment for issue #{issue_id}: #{result[:stderr]}" unless result[:success]
|
|
139
|
-
end
|
|
140
|
-
end
|
|
141
|
-
|
|
142
|
-
def self.delete_comment(issue_id:, comment:)
|
|
143
|
-
comment_id = comment_api_id(comment)
|
|
144
|
-
GhCliExecutor.execute("api", [
|
|
145
|
-
"repos/{owner}/{repo}/issues/comments/#{comment_id}",
|
|
146
|
-
"--method", "DELETE"
|
|
147
|
-
]).tap do |result|
|
|
148
|
-
raise "Failed to delete sticky comment for issue #{issue_id}: #{result[:stderr]}" unless result[:success]
|
|
149
|
-
end
|
|
150
|
-
end
|
|
151
|
-
|
|
152
|
-
def self.comment_api_id(comment)
|
|
153
|
-
database_id = comment["databaseId"] || comment[:databaseId]
|
|
154
|
-
return database_id.to_s if database_id.to_s.match?(/\A\d+\z/)
|
|
155
|
-
|
|
156
|
-
url = comment["url"] || comment[:url]
|
|
157
|
-
return Regexp.last_match(1) if url.to_s.match(/issuecomment-(\d+)/)
|
|
158
|
-
|
|
159
|
-
(comment["id"] || comment[:id]).to_s
|
|
160
|
-
end
|
|
161
|
-
|
|
162
|
-
def self.ensure_label(issue_id, labels)
|
|
163
|
-
return if labels.any? { |label| label["name"] == TRACKED_LABEL }
|
|
164
|
-
|
|
165
|
-
result = GhCliExecutor.execute("issue", ["edit", issue_id.to_s, "--add-label", TRACKED_LABEL])
|
|
166
|
-
raise "Failed to add #{TRACKED_LABEL} label to issue #{issue_id}: #{result[:stderr]}" unless result[:success]
|
|
167
|
-
end
|
|
168
|
-
|
|
169
|
-
def self.remove_label(issue_id, labels)
|
|
170
|
-
return unless labels.any? { |label| label["name"] == TRACKED_LABEL }
|
|
171
|
-
|
|
172
|
-
result = GhCliExecutor.execute("issue", ["edit", issue_id.to_s, "--remove-label", TRACKED_LABEL])
|
|
173
|
-
raise "Failed to remove #{TRACKED_LABEL} label from issue #{issue_id}: #{result[:stderr]}" unless result[:success]
|
|
174
|
-
end
|
|
175
|
-
|
|
176
|
-
def self.validate_link!(issue_id:, task_id:, previous_task_id: nil, lines: nil)
|
|
177
|
-
issue = fetch_issue(issue_id)
|
|
178
|
-
sticky = find_sticky_comment(issue["comments"] || [])
|
|
179
|
-
lines ||= tracked_lines(sticky&.dig("body"))
|
|
180
|
-
owners = lines.map { |line| extract_task_id(line) }.compact.uniq
|
|
181
|
-
return true if owners.empty?
|
|
182
|
-
return true if owners == [task_id.to_s]
|
|
183
|
-
return true if previous_task_id && owners == [previous_task_id.to_s]
|
|
184
|
-
|
|
185
|
-
raise OwnershipConflict, "GitHub issue ##{issue_id} is already owned by task #{owners.first}"
|
|
186
|
-
end
|
|
187
|
-
|
|
188
|
-
def self.sync_lifecycle(issue_id:, issue_state:, task_status:)
|
|
189
|
-
terminal = TERMINAL_STATUSES.include?(task_status.to_s.downcase)
|
|
190
|
-
issue_open = issue_state.to_s.upcase == "OPEN"
|
|
191
|
-
|
|
192
|
-
if terminal && issue_open
|
|
193
|
-
close_result = GhCliExecutor.execute("issue", ["close", issue_id.to_s])
|
|
194
|
-
raise "Failed to close issue #{issue_id}: #{close_result[:stderr]}" unless close_result[:success]
|
|
195
|
-
elsif !terminal && !issue_open
|
|
196
|
-
reopen_result = GhCliExecutor.execute("issue", ["reopen", issue_id.to_s])
|
|
197
|
-
raise "Failed to reopen issue #{issue_id}: #{reopen_result[:stderr]}" unless reopen_result[:success]
|
|
198
|
-
end
|
|
199
|
-
end
|
|
200
|
-
|
|
201
|
-
def self.task_url(task_path)
|
|
202
|
-
return task_path.to_s if task_path.to_s.empty?
|
|
203
|
-
|
|
204
|
-
repo_root = repo_root_path
|
|
205
|
-
relative = if repo_root && task_path.start_with?(repo_root + "/")
|
|
206
|
-
task_path.delete_prefix(repo_root + "/")
|
|
207
|
-
else
|
|
208
|
-
task_path.to_s
|
|
209
|
-
end
|
|
210
|
-
|
|
211
|
-
repo_slug = github_repo_slug
|
|
212
|
-
return relative unless repo_slug
|
|
213
|
-
|
|
214
|
-
"https://github.com/#{repo_slug}/blob/HEAD/#{relative}"
|
|
215
|
-
end
|
|
216
|
-
|
|
217
|
-
def self.repo_root_path
|
|
218
|
-
stdout, _stderr, status = Open3.capture3("git", "rev-parse", "--show-toplevel")
|
|
219
|
-
return nil unless status.success?
|
|
220
|
-
|
|
221
|
-
stdout.strip
|
|
222
|
-
rescue
|
|
223
|
-
nil
|
|
224
|
-
end
|
|
225
|
-
|
|
226
|
-
def self.github_repo_slug
|
|
227
|
-
stdout, _stderr, status = Open3.capture3("git", "remote", "get-url", "origin")
|
|
228
|
-
return nil unless status.success?
|
|
229
|
-
|
|
230
|
-
url = stdout.strip
|
|
231
|
-
return Regexp.last_match(1) if url.match(%r{github\.com[:/](.+?)(?:\.git)?$})
|
|
232
|
-
|
|
233
|
-
nil
|
|
234
|
-
rescue
|
|
235
|
-
nil
|
|
236
|
-
end
|
|
237
|
-
|
|
238
|
-
private_class_method :sync_issue, :fetch_issue, :find_sticky_comment, :tracked_lines, :remove_previous_line,
|
|
239
|
-
:upsert_line, :tracked_line, :extract_task_id, :render_body, :create_comment, :update_comment, :delete_comment,
|
|
240
|
-
:comment_api_id, :ensure_label, :remove_label, :cleanup_tracking_artifacts, :sync_lifecycle, :task_url, :repo_root_path,
|
|
241
|
-
:github_repo_slug
|
|
242
|
-
end
|
|
243
|
-
|
|
244
|
-
# Backward-compatible alias for early integration experiments.
|
|
245
|
-
IssueSync = GithubIssueSync
|
|
246
|
-
end
|
|
247
|
-
end
|
|
248
|
-
end
|