feat: update authored pull branches from mobile (Closes #1360)
This commit is contained in:
parent
a9becd079f
commit
b85fe4a0a3
|
|
@ -800,6 +800,9 @@ 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,6 +1722,11 @@
|
||||||
<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,6 +231,46 @@ 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');
|
||||||
|
|
@ -238,6 +278,7 @@ 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));
|
||||||
|
|
@ -577,6 +618,7 @@ 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));
|
||||||
|
|
@ -650,6 +692,7 @@ 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;
|
||||||
|
|
@ -975,6 +1018,20 @@ 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,6 +173,10 @@ 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."""
|
||||||
|
|
||||||
|
|
@ -2089,6 +2093,58 @@ 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,6 +6721,40 @@ 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,6 +122,53 @@ 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,6 +8,76 @@ 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