Compare commits
No commits in common. "ae98564d9b9de3fb07673eb92d929f0ac23c1921" and "a9becd079fca926c169ef36f7257e4cabea86f98" have entirely different histories.
ae98564d9b
...
a9becd079f
|
|
@ -800,9 +800,6 @@ textarea { resize: vertical; min-height: 120px; }
|
||||||
.ci-check-log { max-width:100%; max-height:42vh; margin:8px 0; padding:10px; overflow-x:auto; overflow-y:auto; border:1px solid #2a496e; border-radius:6px; background:#07101d; color:#dbeafe; font-size:.78rem; white-space:pre; }
|
.ci-check-log { max-width:100%; max-height:42vh; margin:8px 0; padding:10px; overflow-x:auto; overflow-y:auto; border:1px solid #2a496e; border-radius:6px; background:#07101d; color:#dbeafe; font-size:.78rem; white-space:pre; }
|
||||||
.ci-check-recovery-actions { position:sticky; bottom:0; display:grid; grid-template-columns:1fr 1fr; gap:8px; padding-top:6px; background:#101b2e; }
|
.ci-check-recovery-actions { position:sticky; bottom:0; display:grid; grid-template-columns:1fr 1fr; gap:8px; padding-top:6px; background:#101b2e; }
|
||||||
.ci-check-recovery-actions button { min-height:44px; }
|
.ci-check-recovery-actions button { min-height:44px; }
|
||||||
.pull-branch-update { max-width:100%; min-width:0; margin:10px 0; padding:12px; border:1px solid #60a5fa; border-radius:10px; overflow-wrap:anywhere; }
|
|
||||||
.pull-branch-update h3 { margin:0 0 6px; }
|
|
||||||
.pull-branch-update button { width:100%; min-height:44px; }
|
|
||||||
.update-sheet { position:fixed; inset:0; z-index:55; display:none; justify-content:flex-end; background:rgba(5,12,21,.72); backdrop-filter:blur(4px); }
|
.update-sheet { position:fixed; inset:0; z-index:55; display:none; justify-content:flex-end; background:rgba(5,12,21,.72); backdrop-filter:blur(4px); }
|
||||||
.update-sheet.open { display:flex; }
|
.update-sheet.open { display:flex; }
|
||||||
.update-sheet-panel { width:min(560px,100%); height:100%; overflow:auto; padding:18px; background:#0b1526; border-left:1px solid #2a496e; }
|
.update-sheet-panel { width:min(560px,100%); height:100%; overflow:auto; padding:18px; background:#0b1526; border-left:1px solid #2a496e; }
|
||||||
|
|
|
||||||
|
|
@ -1722,11 +1722,6 @@
|
||||||
<div id="pull-review-status" class="small" aria-live="polite">Expand to load changed files and merge readiness.</div>
|
<div id="pull-review-status" class="small" aria-live="polite">Expand to load changed files and merge readiness.</div>
|
||||||
<button class="pull-retry" id="pull-review-retry" type="button" hidden>Retry review data</button>
|
<button class="pull-retry" id="pull-review-retry" type="button" hidden>Retry review data</button>
|
||||||
<div class="row"><span class="pill" id="pull-ci-state">CI unknown</span><span class="pill" id="pull-merge-state">Review data not loaded</span></div>
|
<div class="row"><span class="pill" id="pull-ci-state">CI unknown</span><span class="pill" id="pull-merge-state">Review data not loaded</span></div>
|
||||||
<section class="pull-branch-update" id="pull-branch-update" aria-labelledby="pull-branch-update-heading" hidden>
|
|
||||||
<h3 id="pull-branch-update-heading">Bring branch up to date</h3>
|
|
||||||
<p class="small" id="pull-branch-update-status" role="status" aria-live="assertive"></p>
|
|
||||||
<button id="update-pull-branch" type="button">Update branch</button>
|
|
||||||
</section>
|
|
||||||
<section class="pull-reviewer-summary" aria-labelledby="pull-reviewer-heading">
|
<section class="pull-reviewer-summary" aria-labelledby="pull-reviewer-heading">
|
||||||
<h3 id="pull-reviewer-heading">Reviewer status</h3>
|
<h3 id="pull-reviewer-heading">Reviewer status</h3>
|
||||||
<div id="pull-reviewer-status" class="small" aria-live="polite">Reviewer status not loaded.</div>
|
<div id="pull-reviewer-status" class="small" aria-live="polite">Reviewer status not loaded.</div>
|
||||||
|
|
|
||||||
|
|
@ -231,46 +231,6 @@ function resetReviewRequestControls(doc, detail) {
|
||||||
qs('#pull-feedback-status').textContent = '';
|
qs('#pull-feedback-status').textContent = '';
|
||||||
}
|
}
|
||||||
|
|
||||||
function resetBranchUpdateControls(doc, detail) {
|
|
||||||
const panel = doc.querySelector('#pull-branch-update');
|
|
||||||
if (!panel) return;
|
|
||||||
const eligible = detail?.capabilities?.authored === true && detail?.state === 'open' &&
|
|
||||||
detail?.draft !== true && detail?.merged !== true && detail?.mergeable === false && detail?.head_sha;
|
|
||||||
panel.hidden = !eligible;
|
|
||||||
doc.querySelector('#update-pull-branch').disabled = false;
|
|
||||||
doc.querySelector('#pull-branch-update-status').textContent = eligible ?
|
|
||||||
'Merge the latest base branch into this pull request. Reviews and checks may run again.' : '';
|
|
||||||
}
|
|
||||||
|
|
||||||
function bindBranchUpdateControls(doc, controller, getSelected, getDetail) {
|
|
||||||
const button = doc.querySelector('#update-pull-branch');
|
|
||||||
if (!button || button.dataset.bound === 'true') return;
|
|
||||||
button.dataset.bound = 'true';
|
|
||||||
button.addEventListener('click', async () => {
|
|
||||||
const item = getSelected?.();
|
|
||||||
const detail = getDetail?.();
|
|
||||||
if (!item || !detail?.head_sha || detail?.capabilities?.authored !== true ||
|
|
||||||
!globalThis.confirm('Update ' + item.key + ' with the latest base branch? Reviews and checks may run again.')) return;
|
|
||||||
button.disabled = true;
|
|
||||||
const status = doc.querySelector('#pull-branch-update-status');
|
|
||||||
status.textContent = 'Updating branch…';
|
|
||||||
try {
|
|
||||||
const result = await controller.updateBranch(item, detail.head_sha);
|
|
||||||
if (getSelected?.() !== item || getDetail?.() !== detail) return;
|
|
||||||
Object.assign(detail, result, { mergeable:null, ci_state:'pending', reviewers:[] });
|
|
||||||
status.textContent = 'Branch updated. Refreshing checks and review state…';
|
|
||||||
doc.querySelector('#pull-review-retry')?.click();
|
|
||||||
status.textContent = 'Branch updated to ' + result.head_sha.slice(0, 8) + '. Review the refreshed checks before merging.';
|
|
||||||
button.hidden = true;
|
|
||||||
doc.querySelector('#refresh-pull-checks')?.focus();
|
|
||||||
} catch (error) {
|
|
||||||
status.textContent = error.message + ' Reload the latest pull request state before retrying.';
|
|
||||||
button.disabled = false;
|
|
||||||
button.focus();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function bindReviewRequestControls(doc, controller, getSelected, getDetail, getLogin) {
|
function bindReviewRequestControls(doc, controller, getSelected, getDetail, getLogin) {
|
||||||
const qs = selector => doc.querySelector(selector);
|
const qs = selector => doc.querySelector(selector);
|
||||||
const load = qs('#load-pull-reviewers');
|
const load = qs('#load-pull-reviewers');
|
||||||
|
|
@ -278,7 +238,6 @@ function bindReviewRequestControls(doc, controller, getSelected, getDetail, getL
|
||||||
load.dataset.reviewRequestBound = 'true';
|
load.dataset.reviewRequestBound = 'true';
|
||||||
controller.setReviewDetail = detail => {
|
controller.setReviewDetail = detail => {
|
||||||
resetReviewRequestControls(doc, detail);
|
resetReviewRequestControls(doc, detail);
|
||||||
resetBranchUpdateControls(doc, detail);
|
|
||||||
applyOwnershipCapabilities(doc, detail?.capabilities);
|
applyOwnershipCapabilities(doc, detail?.capabilities);
|
||||||
resetLifecycleControls(doc, detail?.saved_at ? null : detail);
|
resetLifecycleControls(doc, detail?.saved_at ? null : detail);
|
||||||
controller.edit?.setDetail(detail, Boolean(detail?.saved_at));
|
controller.edit?.setDetail(detail, Boolean(detail?.saved_at));
|
||||||
|
|
@ -618,7 +577,6 @@ function bindContextEditor(doc, controller, getSelected, getDetail, getLogin) {
|
||||||
|
|
||||||
function bindOwnershipControls(doc, controller, getSelected, finish, getDetail, getLogin) {
|
function bindOwnershipControls(doc, controller, getSelected, finish, getDetail, getLogin) {
|
||||||
bindReviewRequestControls(doc, controller, getSelected, getDetail, getLogin);
|
bindReviewRequestControls(doc, controller, getSelected, getDetail, getLogin);
|
||||||
bindBranchUpdateControls(doc, controller, getSelected, getDetail);
|
|
||||||
bindFeedbackControls(doc, controller, getSelected, getDetail, getLogin);
|
bindFeedbackControls(doc, controller, getSelected, getDetail, getLogin);
|
||||||
bindCheckRecovery(doc, controller, getSelected, getDetail);
|
bindCheckRecovery(doc, controller, getSelected, getDetail);
|
||||||
bindLifecycleControls(doc, controller, getSelected, getDetail, result => controller.onState?.(result));
|
bindLifecycleControls(doc, controller, getSelected, getDetail, result => controller.onState?.(result));
|
||||||
|
|
@ -692,7 +650,6 @@ function createPullSheet({ fetchJson, storage, onState, createConversationPager
|
||||||
let mergeRequest = null;
|
let mergeRequest = null;
|
||||||
let checkRequest = null;
|
let checkRequest = null;
|
||||||
let ownershipRequest = null;
|
let ownershipRequest = null;
|
||||||
let branchUpdateRequest = null;
|
|
||||||
let candidateRequest = null;
|
let candidateRequest = null;
|
||||||
let reviewCandidateRequest = null;
|
let reviewCandidateRequest = null;
|
||||||
let reviewRequestMutation = null;
|
let reviewRequestMutation = null;
|
||||||
|
|
@ -1018,20 +975,6 @@ function createPullSheet({ fetchJson, storage, onState, createConversationPager
|
||||||
}).finally(() => { mergeRequest = null; });
|
}).finally(() => { mergeRequest = null; });
|
||||||
return mergeRequest;
|
return mergeRequest;
|
||||||
},
|
},
|
||||||
updateBranch(item, expectedHeadSha) {
|
|
||||||
if (branchUpdateRequest) return branchUpdateRequest;
|
|
||||||
branchUpdateRequest = fetchJson(pathFor(item) + '/update-branch', {
|
|
||||||
method: 'POST',
|
|
||||||
headers: { Accept: 'application/json', 'Content-Type': 'application/json' },
|
|
||||||
body: JSON.stringify({ expected_head_sha: expectedHeadSha }),
|
|
||||||
}).then(result => {
|
|
||||||
if (!result?.head_sha || result.head_sha === expectedHeadSha) {
|
|
||||||
throw new Error('Branch update was not confirmed.');
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}).finally(() => { branchUpdateRequest = null; });
|
|
||||||
return branchUpdateRequest;
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -173,10 +173,6 @@ class IssueEditConflictError(ValueError):
|
||||||
"""Raised when an issue changed after the editor loaded it."""
|
"""Raised when an issue changed after the editor loaded it."""
|
||||||
|
|
||||||
|
|
||||||
class PullUpdateConflictError(ValueError):
|
|
||||||
"""Raised when Gitea cannot merge a pull request's base into its head."""
|
|
||||||
|
|
||||||
|
|
||||||
class IssueDependencyInvalidError(ValueError):
|
class IssueDependencyInvalidError(ValueError):
|
||||||
"""Raised when a requested blocker relationship is not valid."""
|
"""Raised when a requested blocker relationship is not valid."""
|
||||||
|
|
||||||
|
|
@ -2093,58 +2089,6 @@ async def reopen_authored_pull(
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
async def update_authored_pull_branch(
|
|
||||||
repository: str, number: int, expected_head_sha: str
|
|
||||||
) -> dict:
|
|
||||||
login, pull = await _current_login_and_target(
|
|
||||||
f"repos/{repository}/pulls/{number}"
|
|
||||||
)
|
|
||||||
author = pull.get("user") if isinstance(pull.get("user"), dict) else {}
|
|
||||||
head = pull.get("head") if isinstance(pull.get("head"), dict) else {}
|
|
||||||
if (
|
|
||||||
pull.get("state") != "open"
|
|
||||||
or pull.get("merged") is True
|
|
||||||
or pull.get("draft") is True
|
|
||||||
or author.get("login", "").casefold() != login.casefold()
|
|
||||||
or head.get("sha") != expected_head_sha
|
|
||||||
):
|
|
||||||
raise IssueNotAvailableError("Pull request state changed")
|
|
||||||
response = await _get_client().post(
|
|
||||||
f"/api/v1/repos/{repository}/pulls/{number}/update",
|
|
||||||
headers=_auth(),
|
|
||||||
params={"style": "merge"},
|
|
||||||
)
|
|
||||||
if response.status_code in {409, 422}:
|
|
||||||
raise PullUpdateConflictError("Pull request has merge conflicts")
|
|
||||||
response.raise_for_status()
|
|
||||||
confirmed_response = await _get_client().get(
|
|
||||||
f"/api/v1/repos/{repository}/pulls/{number}", headers=_auth()
|
|
||||||
)
|
|
||||||
confirmed_response.raise_for_status()
|
|
||||||
confirmed = confirmed_response.json()
|
|
||||||
confirmed_head = confirmed.get("head") if isinstance(confirmed, dict) and isinstance(confirmed.get("head"), dict) else {}
|
|
||||||
new_head_sha = confirmed_head.get("sha")
|
|
||||||
if (
|
|
||||||
not isinstance(confirmed, dict)
|
|
||||||
or confirmed.get("number") != number
|
|
||||||
or confirmed.get("state") != "open"
|
|
||||||
or confirmed.get("merged") is True
|
|
||||||
or not isinstance(new_head_sha, str)
|
|
||||||
or not new_head_sha
|
|
||||||
or new_head_sha == expected_head_sha
|
|
||||||
):
|
|
||||||
raise ValueError("Gitea did not confirm a new pull request head")
|
|
||||||
return {
|
|
||||||
"repository": repository,
|
|
||||||
"number": number,
|
|
||||||
"title": confirmed.get("title", ""),
|
|
||||||
"previous_head_sha": expected_head_sha,
|
|
||||||
"head_sha": new_head_sha,
|
|
||||||
"state": "open",
|
|
||||||
"draft": confirmed.get("draft") is True,
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
async def request_assigned_pull_review(
|
async def request_assigned_pull_review(
|
||||||
repository: str, number: int, reviewer: str, expected_head_sha: str
|
repository: str, number: int, reviewer: str, expected_head_sha: str
|
||||||
) -> dict:
|
) -> dict:
|
||||||
|
|
|
||||||
34
src/main.py
34
src/main.py
|
|
@ -6721,40 +6721,6 @@ async def reopen_authored_pull(
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@app.post("/api/v1/repos/{owner}/{repo}/pulls/{number}/update-branch")
|
|
||||||
async def update_authored_pull_branch(
|
|
||||||
update: PullReadyRequest,
|
|
||||||
owner: str,
|
|
||||||
repo: str,
|
|
||||||
number: int = PathParam(gt=0),
|
|
||||||
):
|
|
||||||
repository = f"{owner}/{repo}"
|
|
||||||
try:
|
|
||||||
result = await asyncio.wait_for(
|
|
||||||
gitea_proxy.update_authored_pull_branch(
|
|
||||||
repository, number, update.expected_head_sha
|
|
||||||
),
|
|
||||||
timeout=ISSUE_ACTION_TIMEOUT_SECONDS,
|
|
||||||
)
|
|
||||||
except gitea_proxy.IssueNotAvailableError:
|
|
||||||
return JSONResponse(
|
|
||||||
{"error": "The pull request changed. Reload the latest state before updating its branch."},
|
|
||||||
status_code=409,
|
|
||||||
)
|
|
||||||
except gitea_proxy.PullUpdateConflictError:
|
|
||||||
return JSONResponse(
|
|
||||||
{"error": "Automatic branch update is unavailable because the branches have conflicts."},
|
|
||||||
status_code=422,
|
|
||||||
)
|
|
||||||
except Exception:
|
|
||||||
return JSONResponse(
|
|
||||||
{"error": "The branch could not be updated. Please retry."},
|
|
||||||
status_code=503,
|
|
||||||
headers={"Retry-After": "1"},
|
|
||||||
)
|
|
||||||
return JSONResponse(result)
|
|
||||||
|
|
||||||
|
|
||||||
@app.get("/api/v1/repos/{owner}/{repo}/pulls/{number}/review-data")
|
@app.get("/api/v1/repos/{owner}/{repo}/pulls/{number}/review-data")
|
||||||
async def assigned_pull_review_data(
|
async def assigned_pull_review_data(
|
||||||
owner: str, repo: str, number: int = PathParam(gt=0)
|
owner: str, repo: str, number: int = PathParam(gt=0)
|
||||||
|
|
|
||||||
|
|
@ -122,53 +122,6 @@ process.stdout.write(JSON.stringify(buildMyWork({json.dumps(payload)})));
|
||||||
assert result[0]["kind"] == "pull"
|
assert result[0]["kind"] == "pull"
|
||||||
|
|
||||||
|
|
||||||
def test_authored_pull_branch_update_is_single_flight_and_head_guarded():
|
|
||||||
script = f"""
|
|
||||||
const createPullSheet = require({json.dumps(str(PULL_SHEET))});
|
|
||||||
const calls = [];
|
|
||||||
let resolve;
|
|
||||||
const pending = new Promise(done => {{ resolve = done; }});
|
|
||||||
const controller = createPullSheet({{
|
|
||||||
fetchJson:(url, options) => {{ calls.push([url, options]); return pending; }},
|
|
||||||
storage:{{}},
|
|
||||||
}});
|
|
||||||
const item = {{repository:'stackchain/api', number:7}};
|
|
||||||
const first = controller.updateBranch(item, 'abc1234');
|
|
||||||
const second = controller.updateBranch(item, 'abc1234');
|
|
||||||
resolve({{previous_head_sha:'abc1234', head_sha:'def5678'}});
|
|
||||||
Promise.all([first, second]).then(results => process.stdout.write(JSON.stringify({{
|
|
||||||
same:first === second,
|
|
||||||
calls:calls.map(([url, options]) => [url, options.method, JSON.parse(options.body)]),
|
|
||||||
heads:results.map(result => result.head_sha),
|
|
||||||
}}))).catch(error => {{ console.error(error); process.exit(1); }});
|
|
||||||
"""
|
|
||||||
completed = subprocess.run(["node", "-e", script], text=True, capture_output=True)
|
|
||||||
|
|
||||||
assert completed.returncode == 0, completed.stderr
|
|
||||||
assert json.loads(completed.stdout) == {
|
|
||||||
"same": True,
|
|
||||||
"calls": [[
|
|
||||||
"api/v1/repos/stackchain/api/pulls/7/update-branch",
|
|
||||||
"POST",
|
|
||||||
{"expected_head_sha": "abc1234"},
|
|
||||||
]],
|
|
||||||
"heads": ["def5678", "def5678"],
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def test_mobile_pull_review_exposes_update_branch_recovery():
|
|
||||||
frontend = Path(__file__).parents[1] / "frontend"
|
|
||||||
html = (frontend / "index.html").read_text()
|
|
||||||
source = PULL_SHEET.read_text()
|
|
||||||
css = (frontend / "dashboard.css").read_text()
|
|
||||||
|
|
||||||
assert 'id="update-pull-branch"' in html
|
|
||||||
assert 'id="pull-branch-update-status"' in html
|
|
||||||
assert "controller.updateBranch(item, detail.head_sha)" in source
|
|
||||||
assert "doc.querySelector('#pull-review-retry')?.click()" in source
|
|
||||||
assert ".pull-branch-update button" in css and "min-height:44px" in css
|
|
||||||
|
|
||||||
|
|
||||||
def test_my_prs_filter_and_count_include_only_authored_pulls():
|
def test_my_prs_filter_and_count_include_only_authored_pulls():
|
||||||
script = f"""
|
script = f"""
|
||||||
const buildMyWork = require({json.dumps(str(MY_WORK))});
|
const buildMyWork = require({json.dumps(str(MY_WORK))});
|
||||||
|
|
|
||||||
|
|
@ -8,76 +8,6 @@ from src import gitea_proxy, main
|
||||||
from src.security_event_store import SecurityEventStoreError
|
from src.security_event_store import SecurityEventStoreError
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.anyio
|
|
||||||
async def test_gitea_updates_authored_pull_branch_and_confirms_new_head():
|
|
||||||
requests = []
|
|
||||||
pull = {
|
|
||||||
"number": 7, "title": "Ship mobile flow", "state": "open", "draft": False,
|
|
||||||
"merged": False, "mergeable": False, "user": {"login": "alex"},
|
|
||||||
"head": {"sha": "abc1234"},
|
|
||||||
}
|
|
||||||
|
|
||||||
async def handler(request):
|
|
||||||
requests.append((request.method, request.url.path, request.content))
|
|
||||||
if request.url.path == "/api/v1/user":
|
|
||||||
return httpx.Response(200, json={"login": "alex"})
|
|
||||||
if request.url.path == "/api/v1/repos/stackchain/api/pulls/7" and request.method == "GET":
|
|
||||||
return httpx.Response(200, json=pull)
|
|
||||||
if request.url.path == "/api/v1/repos/stackchain/api/pulls/7/update":
|
|
||||||
assert request.method == "POST"
|
|
||||||
pull["head"] = {"sha": "def5678"}
|
|
||||||
return httpx.Response(200, json={"message": "updated"})
|
|
||||||
raise AssertionError(f"unexpected request: {request.method} {request.url.path}")
|
|
||||||
|
|
||||||
gitea_proxy.start_client(transport=httpx.MockTransport(handler))
|
|
||||||
try:
|
|
||||||
result = await gitea_proxy.update_authored_pull_branch(
|
|
||||||
"stackchain/api", 7, "abc1234"
|
|
||||||
)
|
|
||||||
finally:
|
|
||||||
await gitea_proxy.stop_client()
|
|
||||||
|
|
||||||
assert result == {
|
|
||||||
"repository": "stackchain/api", "number": 7, "title": "Ship mobile flow",
|
|
||||||
"previous_head_sha": "abc1234", "head_sha": "def5678", "state": "open",
|
|
||||||
"draft": False,
|
|
||||||
}
|
|
||||||
assert [item[:2] for item in requests] == [
|
|
||||||
("GET", "/api/v1/user"),
|
|
||||||
("GET", "/api/v1/repos/stackchain/api/pulls/7"),
|
|
||||||
("POST", "/api/v1/repos/stackchain/api/pulls/7/update"),
|
|
||||||
("GET", "/api/v1/repos/stackchain/api/pulls/7"),
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.anyio
|
|
||||||
async def test_update_pull_branch_api_maps_stale_head_and_conflict(monkeypatch):
|
|
||||||
outcomes = [
|
|
||||||
gitea_proxy.IssueNotAvailableError("stale"),
|
|
||||||
gitea_proxy.PullUpdateConflictError("conflict"),
|
|
||||||
]
|
|
||||||
|
|
||||||
async def update(*_args):
|
|
||||||
raise outcomes.pop(0)
|
|
||||||
|
|
||||||
monkeypatch.setattr(main.gitea_proxy, "update_authored_pull_branch", update, raising=False)
|
|
||||||
transport = httpx.ASGITransport(app=main.app)
|
|
||||||
async with httpx.AsyncClient(transport=transport, base_url="http://test") as client:
|
|
||||||
stale = await client.post(
|
|
||||||
"/api/v1/repos/stackchain/api/pulls/7/update-branch",
|
|
||||||
json={"expected_head_sha": "abc1234"},
|
|
||||||
)
|
|
||||||
conflict = await client.post(
|
|
||||||
"/api/v1/repos/stackchain/api/pulls/7/update-branch",
|
|
||||||
json={"expected_head_sha": "abc1234"},
|
|
||||||
)
|
|
||||||
|
|
||||||
assert stale.status_code == 409
|
|
||||||
assert "Reload" in stale.json()["error"]
|
|
||||||
assert conflict.status_code == 422
|
|
||||||
assert "conflicts" in conflict.json()["error"]
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.anyio
|
@pytest.mark.anyio
|
||||||
async def test_gitea_closes_authored_pull_at_expected_head_and_verifies_state():
|
async def test_gitea_closes_authored_pull_at_expected_head_and_verifies_state():
|
||||||
requests = []
|
requests = []
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user