///|
fn default_hub_merge_policy() -> @hub.PrMergePolicy {
@hub.PrMergePolicy::default()
}
///|
fn default_hub_merge_policy_toml() -> String {
let template =
#|# Hub merge policy
#|# required_approvals: minimum number of latest approved reviews
#|# allow_request_changes: allow merge while latest review requests changes
#|# require_signed_records: require PR metadata and reviews to be signed
#|# required_workflows: workflow task names that must be latest-success
#|[merge]
#|required_approvals = 0
#|allow_request_changes = true
#|require_signed_records = false
#|required_workflows = []
template
}
///|
fn parse_merge_policy_bool(
value : String,
key : String,
) -> Bool raise @bitcore.GitError {
let normalized = value.to_lower()
match normalized {
"true" | "1" | "yes" => true
"false" | "0" | "no" => false
_ =>
raise @bitcore.GitError::InvalidObject(
"hub merge policy: '\{key}' must be true or false: \{value}",
)
}
}
///|
fn parse_merge_policy_string_array(
value : String,
key : String,
) -> Array[String] raise @bitcore.GitError {
let parsed = @json.parse(value) catch {
_ =>
raise @bitcore.GitError::InvalidObject(
"hub merge policy: '\{key}' must be JSON string array: \{value}",
)
}
match parsed {
Json::Array(items) => {
let out : Array[String] = []
for item in items {
match item {
Json::String(text) => out.push(text)
_ =>
raise @bitcore.GitError::InvalidObject(
"hub merge policy: '\{key}' must contain only strings",
)
}
}
out
}
_ =>
raise @bitcore.GitError::InvalidObject(
"hub merge policy: '\{key}' must be string array",
)
}
}
///|
fn parse_hub_merge_policy_toml(
content : String,
) -> @hub.PrMergePolicy raise @bitcore.GitError {
let mut required_approvals = 0
let mut allow_request_changes = true
let mut require_signed_records = false
let mut required_workflows : Array[String] = []
let mut in_merge_section = false
for line_view in content.split("\n") {
let raw_line = line_view.to_owned()
let line = trim_string(raw_line)
if line.length() == 0 || line.has_prefix("#") || line.has_prefix(";") {
continue
}
if line.has_prefix("[") && line.has_suffix("]") {
let section_name = trim_string(
String::unsafe_substring(line, start=1, end=line.length() - 1),
)
in_merge_section = section_name == "merge"
continue
}
if !in_merge_section {
continue
}
let eq_idx = line.find("=")
guard eq_idx is Some(i) else {
raise @bitcore.GitError::InvalidObject(
"hub merge policy: expected key=value in [merge]: \{line}",
)
}
let key = trim_string(String::unsafe_substring(line, start=0, end=i))
let value_part = trim_string(
String::unsafe_substring(line, start=i + 1, end=line.length()),
)
let value = match value_part.find("#") {
Some(comment_start) =>
trim_string(
String::unsafe_substring(value_part, start=0, end=comment_start),
)
None => value_part
}
match key {
"required_approvals" => {
let parsed = @string.parse_int(value) catch {
_ =>
raise @bitcore.GitError::InvalidObject(
"hub merge policy: required_approvals must be int: \{value}",
)
}
if parsed < 0 {
raise @bitcore.GitError::InvalidObject(
"hub merge policy: required_approvals must be >= 0",
)
}
required_approvals = parsed
}
"allow_request_changes" =>
allow_request_changes = parse_merge_policy_bool(value, key)
"require_signed_records" =>
require_signed_records = parse_merge_policy_bool(value, key)
"required_workflows" =>
required_workflows = parse_merge_policy_string_array(value, key)
_ =>
raise @bitcore.GitError::InvalidObject(
"hub merge policy: unknown key in [merge]: \{key}",
)
}
}
@hub.PrMergePolicy::new(
required_approvals,
allow_request_changes,
require_signed_records,
required_workflows~,
)
}
///|
fn hub_merge_policy_path(git_dir : String) -> String {
git_dir + "/hub/policy.toml"
}
///|
fn ensure_hub_merge_policy_file(
fs : OsFs,
git_dir : String,
) -> Unit raise @bitcore.GitError {
let policy_path = hub_merge_policy_path(git_dir)
if fs.is_file(policy_path) {
return ()
}
fs.mkdir_p(git_dir + "/hub")
fs.write_string(policy_path, default_hub_merge_policy_toml())
}
///|
fn load_hub_merge_policy(
fs : OsFs,
git_dir : String,
) -> @hub.PrMergePolicy raise @bitcore.GitError {
let policy_path = hub_merge_policy_path(git_dir)
if !fs.is_file(policy_path) {
return default_hub_merge_policy()
}
let content = decode_bytes(fs.read_file(policy_path))
parse_hub_merge_policy_toml(content)
}
///|
fn parse_positive_list_limit(value : String) -> Int raise @bitcore.GitError {
let parsed = @string.parse_int(value) catch {
_ => raise @bitcore.GitError::InvalidObject("Invalid limit: \{value}")
}
if parsed <= 0 {
raise @bitcore.GitError::InvalidObject("Limit must be positive")
}
parsed
}
///|
fn parse_pr_state_flag(value : String) -> @hub.PrState? {
match parse_work_item_state_flag(value) {
Some(work_item_state) => Some(work_item_state.to_pr_state())
None => None
}
}
///|
fn parse_issue_state_flag(value : String) -> @hub.IssueState? {
match parse_work_item_state_flag(value) {
Some(work_item_state) => work_item_state.to_issue_state()
None => None
}
}
///|
fn parse_work_item_state_flag(value : String) -> @hub.WorkItemState? {
match value {
"--open" => Some(@hub.WorkItemState::Open)
"--closed" => Some(@hub.WorkItemState::Closed)
"--merged" => Some(@hub.WorkItemState::Merged)
_ => None
}
}
///|
fn parse_work_item_kind_flag(value : String) -> @hub.WorkItemKind? {
match value {
"--issue" => Some(@hub.WorkItemKind::Issue)
"--pr" | "--pull-request" => Some(@hub.WorkItemKind::PullRequest)
_ => None
}
}
///|
fn normalize_hub_pr_subcommand(subcmd : String) -> String {
match subcmd {
"view" => "get"
"edit" => "update"
"proposal" => "proposals"
_ => subcmd
}
}
///|
fn normalize_hub_issue_subcommand(subcmd : String) -> String {
match subcmd {
"view" => "get"
"edit" => "update"
_ => subcmd
}
}
///|
fn normalize_hub_work_item_subcommand(subcmd : String) -> String {
match subcmd {
"view" => "get"
_ => subcmd
}
}
///|
fn resolve_hub_comment_subcommand(subcmd : String) -> String {
match subcmd {
"add" | "list" | "help" | "-h" | "--help" => subcmd
_ => "add"
}
}
///|
fn resolve_hub_review_subcommand(subcmd : String) -> String {
match subcmd {
"submit" | "list" | "help" | "-h" | "--help" => subcmd
_ => "submit"
}
}
///|
fn parse_review_verdict_flag(value : String) -> @hub.ReviewVerdict? {
match value {
"--approve" | "--approved" => Some(@hub.ReviewVerdict::Approved)
"--comment" => Some(@hub.ReviewVerdict::Comment)
"--request-changes" => Some(@hub.ReviewVerdict::RequestChanges)
_ => None
}
}
///|
fn parse_merge_strategy_flag(value : String) -> @hub.PrMergeStrategy? {
match value {
"--merge" => Some(@hub.PrMergeStrategy::Merge)
"--squash" => Some(@hub.PrMergeStrategy::Squash)
"--ff" | "--fast-forward" => Some(@hub.PrMergeStrategy::FastForward)
_ => None
}
}
///|
fn parse_pr_state(value : String) -> @hub.PrState? {
match parse_work_item_state(value) {
Some(work_item_state) => Some(work_item_state.to_pr_state())
None => None
}
}
///|
fn parse_issue_state(value : String) -> @hub.IssueState? {
match parse_work_item_state(value) {
Some(work_item_state) => work_item_state.to_issue_state()
None => None
}
}
///|
fn parse_work_item_state(value : String) -> @hub.WorkItemState? {
match value {
"open" => Some(@hub.WorkItemState::Open)
"closed" => Some(@hub.WorkItemState::Closed)
"merged" => Some(@hub.WorkItemState::Merged)
_ => None
}
}
///|
fn parse_work_item_kind(value : String) -> @hub.WorkItemKind? {
match value {
"issue" => Some(@hub.WorkItemKind::Issue)
"pr" | "pull-request" | "pullrequest" =>
Some(@hub.WorkItemKind::PullRequest)
_ => None
}
}
///|
fn parse_review_verdict(value : String) -> @hub.ReviewVerdict? {
match value {
"approved" | "approve" => Some(@hub.ReviewVerdict::Approved)
"comment" => Some(@hub.ReviewVerdict::Comment)
"request-changes" | "request_changes" =>
Some(@hub.ReviewVerdict::RequestChanges)
_ => None
}
}
///|
fn parse_merge_strategy(value : String) -> @hub.PrMergeStrategy? {
match value {
"merge" => Some(@hub.PrMergeStrategy::Merge)
"squash" => Some(@hub.PrMergeStrategy::Squash)
"ff" | "fast-forward" | "fastforward" =>
Some(@hub.PrMergeStrategy::FastForward)
_ => None
}
}
///|
fn format_pr_summary(pr : @hub.PullRequest) -> String {
let id = pr.id()
let title = pr.title()
let state = pr.state().to_string()
"#\{id} [\{state}] \{title}"
}
///|
fn format_issue_summary(issue : @hub.Issue) -> String {
let id = issue.id()
let title = issue.title()
let state = issue.state().to_string()
"#\{id} [\{state}] \{title}"
}
///|
fn format_issue_json(issue : @hub.Issue) -> String {
let labels = issue.labels()
let label_parts : Array[String] = []
for label in labels {
label_parts.push(json_escape_string(label))
}
let assignees = issue.assignees()
let assignee_parts : Array[String] = []
for a in assignees {
assignee_parts.push(json_escape_string(a))
}
let linked = issue.linked_prs()
let linked_parts : Array[String] = []
for p in linked {
linked_parts.push(json_escape_string(p))
}
let parent = match issue.parent_id {
Some(pid) => json_escape_string(pid)
None => "null"
}
let buf = StringBuilder()
buf.write_string("{")
buf.write_string("\"id\":" + json_escape_string(issue.id()))
buf.write_string(",\"title\":" + json_escape_string(issue.title()))
buf.write_string(
",\"state\":" + json_escape_string(issue.state().to_string()),
)
buf.write_string(",\"author\":" + json_escape_string(issue.author()))
buf.write_string(",\"created_at\":" + issue.created_at().to_string())
buf.write_string(",\"updated_at\":" + issue.updated_at().to_string())
buf.write_string(",\"body\":" + json_escape_string(issue.body()))
buf.write_string(",\"labels\":[" + label_parts.join(",") + "]")
buf.write_string(",\"assignees\":[" + assignee_parts.join(",") + "]")
buf.write_string(",\"linked_prs\":[" + linked_parts.join(",") + "]")
buf.write_string(",\"parent_id\":" + parent)
buf.write_string("}")
buf.to_string()
}
///|
///|
/// Format issue detail with human-readable timestamps for CLI display.
/// Replaces raw epoch in serialize output with ISO-style dates.
fn format_issue_detail(issue : @hub.Issue) -> String {
let raw = issue.serialize()
let sb = StringBuilder()
for line_view in raw.split("\n") {
let line = line_view.to_owned()
if line.has_prefix("created ") {
let ts = line[8:].to_owned()
sb.write_string("created ")
sb.write_string(ts)
sb.write_string(" (")
sb.write_string(format_timestamp_iso(parse_int64_safe(ts), "+0000"))
sb.write_string(")")
sb.write_char('\n')
} else if line.has_prefix("updated ") {
let ts = line[8:].to_owned()
sb.write_string("updated ")
sb.write_string(ts)
sb.write_string(" (")
sb.write_string(format_timestamp_iso(parse_int64_safe(ts), "+0000"))
sb.write_string(")")
sb.write_char('\n')
} else {
sb.write_string(line)
sb.write_char('\n')
}
}
// Remove trailing newline
let result = sb.to_string()
if result.length() > 0 && result.unsafe_get(result.length() - 1) == '\n' {
String::unsafe_substring(result, start=0, end=result.length() - 1)
} else {
result
}
}
///|
fn parse_int64_safe(s : String) -> Int64 {
let mut result : Int64 = 0
for ch in s {
if ch >= '0' && ch <= '9' {
result = result * 10L + (ch.to_int() - 48).to_int64()
}
}
result
}
///|
fn json_escape_string(s : String) -> String {
let buf = StringBuilder()
buf.write_string("\"")
for ch in s {
match ch {
'"' => buf.write_string("\\\"")
'\\' => buf.write_string("\\\\")
'\n' => buf.write_string("\\n")
'\r' => buf.write_string("\\r")
'\t' => buf.write_string("\\t")
_ => buf.write_char(ch)
}
}
buf.write_string("\"")
buf.to_string()
}
///|
fn format_work_item_summary(item : @hub.WorkItem) -> String {
let id = item.id()
let title = item.title()
let kind = item.kind().to_string()
let state = item.state().to_string()
"#\{id} [\{kind}/\{state}] \{title}"
}
///|
fn format_work_item_detail(item : @hub.WorkItem) -> String {
let sb = StringBuilder()
sb.write_string("id ")
sb.write_string(item.id())
sb.write_char('\n')
sb.write_string("kind ")
sb.write_string(item.kind().to_string())
sb.write_char('\n')
sb.write_string("state ")
sb.write_string(item.state().to_string())
sb.write_char('\n')
sb.write_string("author ")
sb.write_string(item.author())
sb.write_char('\n')
sb.write_string("created-at ")
sb.write_string(item.created_at().to_string())
sb.write_char('\n')
sb.write_string("updated-at ")
sb.write_string(item.updated_at().to_string())
sb.write_char('\n')
for label in item.labels() {
sb.write_string("label ")
sb.write_string(label)
sb.write_char('\n')
}
for assignee in item.assignees() {
sb.write_string("assignee ")
sb.write_string(assignee)
sb.write_char('\n')
}
for linked_pr in item.linked_prs() {
sb.write_string("linked-pr ")
sb.write_string(linked_pr)
sb.write_char('\n')
}
match item.patch() {
None => ()
Some(patch) => {
sb.write_string("source-branch ")
sb.write_string(patch.source_branch())
sb.write_char('\n')
match patch.source_repo() {
Some(source_repo) => {
sb.write_string("source-repo ")
sb.write_string(source_repo)
sb.write_char('\n')
}
None => ()
}
match patch.source_ref() {
Some(source_ref) => {
sb.write_string("source-ref ")
sb.write_string(source_ref)
sb.write_char('\n')
}
None => ()
}
sb.write_string("source-commit ")
sb.write_string(patch.source_commit().to_hex())
sb.write_char('\n')
sb.write_string("target-branch ")
sb.write_string(patch.target_branch())
sb.write_char('\n')
sb.write_string("target-commit ")
sb.write_string(patch.target_commit().to_hex())
sb.write_char('\n')
for closes_issue in patch.closes_issues() {
sb.write_string("closes-issue ")
sb.write_string(closes_issue)
sb.write_char('\n')
}
match patch.merge_commit() {
Some(merge_commit) => {
sb.write_string("merge-commit ")
sb.write_string(merge_commit.to_hex())
sb.write_char('\n')
}
None => ()
}
}
}
sb.write_string("title ")
sb.write_string(item.title())
sb.write_char('\n')
sb.write_char('\n')
sb.write_string(item.body())
sb.to_string()
}
///|
fn note_ns(ns : String?) -> String {
ns.unwrap_or("commits")
}