Skip to content

Where people and agents ship software together. The open-source git platform for the whole job: issues, agents, checks and deploys to the edge.

Commit

Work: work items as a module of their own, with their own attachments

services/work/src/items/ is the boundary: numbering, links, landing, references, labels, milestones and the migration live there, and the pull request, merge, push and coordination code reach them only through its functions and read fragments, which the compiler holds them to. A later split into a service of its own turns those into RPC calls. Attachments belong to a work item when they are added to one, so an item filed in the workspace itself can take files: a new upload_work_item_attachment operation, the work_item tool's attach, and g1t work comment and work new --attach. An old issue link now resolves for a member who cannot read its repository, and legacy_item_map says when the move is finished.

G1T-Session:
ses_01m56adwzvenhby748vvbn96c5
syntaqxcommitted Parentd7cfe76Browse files
57 files+199−3441 files not counted0/57 viewed
+3−1
381381 Op::EditWorkItemComment,
382382 Op::DeleteWorkItemComment,
383383 Op::ReactToWorkItem,
384+ Op::UploadWorkItemAttachment,
384385 Op::UploadAttachment,
385386 Op::ListWorkItemLabels,
386387 Op::AddWorkItemLabels,
768769 Op::DeleteWorkItemComment => "Delete a work item comment",
769770 Op::ReactToWorkItem => "React to a work item",
770771 Op::ListWorkItemDecisions => "List a work item's decisions",
772+ Op::UploadWorkItemAttachment => "Upload an attachment to a work item",
771773 Op::UploadAttachment => "Upload an attachment",
772774 Op::ReviewPullRequest => "Review a pull request",
773775 Op::ListPullRequests => "List pull requests",
11541156 if op != Op::Whoami {
11551157 responses.insert("422".into(), error_response("The input is not valid."));
11561158 }
1157− if op == Op::UploadAttachment {
1159+ if op == Op::UploadAttachment || op == Op::UploadWorkItemAttachment {
11581160 responses.insert("413".into(), error_response("The file is over 10 MB (`too_large`)."));
11591161 responses.insert("415".into(), error_response("The file is not a PNG, JPEG, GIF or WebP image by its bytes, or not the content_type given (`unsupported_type`)."));
11601162 }
+49−2
202202 DeleteWorkItemComment,
203203 ReactToWorkItem,
204204 ListWorkItemDecisions,
205+ UploadWorkItemAttachment,
205206 UploadAttachment,
206207 ReviewPullRequest,
207208 ListPullRequests,
867868 }
868869
869870 impl Op {
870− pub const ALL: [Op; 427] = [
871+ pub const ALL: [Op; 428] = [
871872 Op::Whoami,
872873 Op::GetWorkspace,
873874 Op::CreateWorkspace,
952953 Op::DeleteWorkItemComment,
953954 Op::ReactToWorkItem,
954955 Op::ListWorkItemDecisions,
956+ Op::UploadWorkItemAttachment,
955957 Op::UploadAttachment,
956958 Op::ReviewPullRequest,
957959 Op::ListPullRequests,
13881390 Op::DeleteWorkItemComment => "delete_work_item_comment",
13891391 Op::ReactToWorkItem => "react_to_work_item",
13901392 Op::ListWorkItemDecisions => "list_work_item_decisions",
1393+ Op::UploadWorkItemAttachment => "upload_work_item_attachment",
13911394 Op::UploadAttachment => "upload_attachment",
13921395 Op::ReviewPullRequest => "review_pull_request",
13931396 Op::ListPullRequests => "list_pull_requests",
17891792 Op::ListWorkItemDecisions => {
17901793 "The decision records on a work item and on its pull requests, oldest first: each with its kind (proposal, claim, integration_failure, integration_passed, fix, approval, merge or note), its confidence (claimed, tested or approved), who recorded it (absent for g1t), its summary and its evidence. The workspace is `workspace`, its slug; with only `repo`, the repository's workspace."
17911794 }
1795+ Op::UploadWorkItemAttachment => {
1796+ "Upload an image to show in a work item's description or in a comment on it, kept by the work item: one filed with no repository takes files too. As upload_attachment otherwise (PNG, JPEG, GIF and WebP of at most 10 MB, 413 too_large, 415 unsupported_type, the same answer with its markdown); anyone who may comment on the work item may. The workspace is `workspace`, its slug; with only `repo`, the repository's workspace."
1797+ }
17921798 Op::UploadAttachment => {
17931799 "Upload an image to show in a work item's or a pull request's description or in a comment: a screenshot of a preview, say, instead of committing it to the repository. data is the file base64-encoded, name its file name (the image's alt text), content_type optional (checked against the bytes). PNG, JPEG, GIF and WebP of at most 10 MB; a larger file is refused with 413 too_large, another type (SVG, PDF, anything its bytes do not show to be one of those) with 415 unsupported_type. Anyone who may comment on the repository may. Returns its id, url (on the usercontent origin, g1tusercontent.com on g1t.sh), name, size, content_type, and markdown (`![name](url)`) to paste into the body or summary of open_work_item, update_work_item, add_work_item_comment, add_comment, create_pull_request or mark_pull_request_ready. Its address is its permission: anyone given it can see the image, even one from a private repository. Over MCP the file travels in the tool call, so keep it small (a few hundred KB); `g1t attach FILE` uploads one from disk and prints the markdown."
17941800 }
29602966 })),
29612967 &["number", "content"],
29622968 ),
2969+ Op::UploadWorkItemAttachment => object(
2970+ item_numbered(json!({
2971+ "name": {
2972+ "type": "string",
2973+ "description": "The file's name, such as preview.png: the image's alt text in the markdown.",
2974+ },
2975+ "content_type": {
2976+ "type": "string",
2977+ "enum": g1t_contracts::attachments::ATTACHMENT_TYPES,
2978+ "description": "What the file is. Optional: it is read from the bytes, and refused when it says otherwise.",
2979+ },
2980+ "data": {
2981+ "type": "string",
2982+ "contentEncoding": "base64",
2983+ "description": "The file, base64-encoded: PNG, JPEG, GIF or WebP of at most 10 MB.",
2984+ },
2985+ })),
2986+ &["number", "name", "data"],
2987+ ),
29632988 Op::UploadAttachment => object(
29642989 json!({
29652990 "repo": repo_schema(),
42414266 | Op::EditWorkItemComment
42424267 | Op::DeleteWorkItemComment
42434268 | Op::ReactToWorkItem
4269+ | Op::UploadWorkItemAttachment
42444270 | Op::ImportWorkItems
42454271 | Op::ListLabels
42464272 | Op::CreateLabel
55245550 )
55255551 .await
55265552 }
5553+ Op::UploadWorkItemAttachment => {
5554+ let asked = g1t_contracts::attachments::UploadAttachmentArgs {
5555+ actor: actor(),
5556+ repo: None,
5557+ workspace: item_ws(),
5558+ number: Some(number),
5559+ name: text(input, "name"),
5560+ content_type: optional_text(input, "content_type"),
5561+ data: text(input, "data"),
5562+ };
5563+ if asked.data.trim().is_empty() {
5564+ return failed(FailureCode::Invalid, "Give data: the file, base64-encoded.");
5565+ }
5566+ let kept: Outcome<g1t_contracts::attachments::Attachment> = call(work, "upload_attachment", &asked).await?;
5567+ match kept {
5568+ Outcome::Ok(attachment) => ok(&attachment_answer(&services.addresses.usercontent, &attachment)),
5569+ Outcome::Fail(failure) => Ok(Outcome::Fail(failure)),
5570+ }
5571+ }
55275572 Op::UpdatePullRequest => {
55285573 // `state` reopens a closed pull request (first, so that the
55295574 // rest can change it) or closes an open one (last).
57485793 Op::UploadAttachment => {
57495794 let asked = g1t_contracts::attachments::UploadAttachmentArgs {
57505795 actor: actor(),
5751− repo,
5796+ repo: Some(repo),
5797+ workspace: String::new(),
5798+ number: None,
57525799 name: text(input, "name"),
57535800 content_type: optional_text(input, "content_type"),
57545801 data: text(input, "data"),
+20−0
16881688 },
16891689 "notes": "`data` is the file base64-encoded: `base64 -w0 preview.png`. Paste `markdown` into the `body` of a work item, pull request or comment, or a pull request's `summary`. The type is read from the bytes; a file over 10 MB is refused with `413` (`too_large`), one that is not a PNG, JPEG, GIF or WebP with `415` (`unsupported_type`). From a shell, `g1t attach preview.png` does all of it and prints the markdown."
16901690 },
1691+ "upload_work_item_attachment": {
1692+ "params": {
1693+ "workspace": "flagon-io",
1694+ "number": 214
1695+ },
1696+ "request": {
1697+ "name": "preview.png",
1698+ "content_type": "image/png",
1699+ "data": "iVBORw0KGgoAAAANSUhEUgAA…"
1700+ },
1701+ "response": {
1702+ "id": "8d2e6b4f1a9c3e7d5b0f2a8c6e4d1b9f7a3c5e0d2b8f6a4c1e9d7b3f5a0c2e8d",
1703+ "name": "preview.png",
1704+ "url": "https://g1tusercontent.com/attachments/8d2e6b4f1a9c3e7d5b0f2a8c6e4d1b9f7a3c5e0d2b8f6a4c1e9d7b3f5a0c2e8d",
1705+ "markdown": "![preview.png](https://g1tusercontent.com/attachments/8d2e6b4f1a9c3e7d5b0f2a8c6e4d1b9f7a3c5e0d2b8f6a4c1e9d7b3f5a0c2e8d)",
1706+ "content_type": "image/png",
1707+ "size": 184213
1708+ },
1709+ "notes": "As upload_attachment, kept by the work item: one filed with no repository takes files too. Paste `markdown` into the work item's `body` or a comment on it. From a shell, `g1t work comment 214 --attach preview.png` uploads it and comments."
1710+ },
16911711 "react": {
16921712 "params": {
16931713 "number": 12,
+1−0
623623 route("DELETE", "/workspaces/:workspace/work/:number/labels/:label", Op::RemoveWorkItemLabels, &[]),
624624 route("POST", "/workspaces/:workspace/work/:number/comments", Op::AddWorkItemComment, &[]),
625625 route("POST", "/workspaces/:workspace/work/:number/reactions", Op::ReactToWorkItem, &[]),
626+ route("POST", "/workspaces/:workspace/work/:number/attachments", Op::UploadWorkItemAttachment, &[]),
626627 route("GET", "/workspaces/:workspace/work/:number/decisions", Op::ListWorkItemDecisions, &[]),
627628 // The work items linked to a repository: its Work tab. Read only.
628629 route("GET", "/repos/:owner/:name/work", Op::ListRepoWorkItems, &[("state", "state"), ("label", "label"), ("milestone", "milestone"), ("assignee", "assignee"), ("role", "role")]),
+4−3
162162 a("delete_comment", Op::DeleteWorkItemComment, "Delete a comment: your own, or any with the Maintain role"),
163163 a("react", Op::ReactToWorkItem, "React with an emoji to it, or with comment_id to one of its comments; remove takes it back"),
164164 a("decisions", Op::ListWorkItemDecisions, "Decision records on it and its pull requests"),
165− a("attach", Op::UploadAttachment, "Upload an image (base64, PNG/JPEG/GIF/WebP, at most 10 MB; keep it small over MCP) for its markdown, to paste into a body, summary or comment"),
165+ a("attach", Op::UploadWorkItemAttachment, "Upload an image (base64, PNG/JPEG/GIF/WebP, at most 10 MB; keep it small over MCP) for its markdown, to paste into its body or a comment"),
166166 a("import", Op::ImportWorkItems, "Open a work item from a Jira, Linear or Sentry item"),
167167 a("resolve", Op::ResolveRef, "What #N names in a workspace, or in repo: a work item or a pull request"),
168168 a("work", Op::Coordination(CoordinationOp::GetWork), "Everything to pick it up: done means, repositories and what landed, missing targets, suggested targets, claims, sessions, pull requests, blockers, decisions"),
191191 a("edit_comment", Op::EditComment, "Change a comment's text: your own, or any with the Maintain role"),
192192 a("delete_comment", Op::DeleteComment, "Delete a comment: your own, or any with the Maintain role"),
193193 a("react", Op::React, "React with an emoji to it, or with comment_id to one of its comments; remove takes it back"),
194+ a("attach", Op::UploadAttachment, "Upload an image (base64, PNG/JPEG/GIF/WebP, at most 10 MB; keep it small over MCP) for its markdown, to paste into its body, its summary or a comment"),
194195 a("ready", Op::MarkPullRequestReady, "Mark a draft ready, with a summary"),
195196 a("draft", Op::ConvertPullRequestToDraft, "Turn it back into a draft"),
196197 a("request_reviewers", Op::RequestReviewers, "Ask people or teams to review it"),
10461047 // Labels and milestones are the workspace's, managed with work:write;
10471048 // looking up the ticket a work item refers to is search's, and a
10481049 // repository's commit comments and its work list are the
1049− // repository's.
1050− assert_eq!(names, vec![json!("search"), json!("repository"), json!("work_item"), json!("workspace"), json!("account")]);
1050+ // repository's; an image for a pull request is its attach.
1051+ assert_eq!(names, vec![json!("search"), json!("repository"), json!("work_item"), json!("pull_request"), json!("workspace"), json!("account")]);
10511052 // Notifications are a resource of their own: reading them lists
10521053 // only what reads.
10531054 let reader = token(Some(vec![Scope::NotificationsRead]));
+0−0

Binary or large file; its contents are not shown.

+21−8
1−//! Files put in an issue's, a pull request's or a comment's Markdown: a
1+//! Files put in a work item's, a pull request's or a comment's Markdown: a
22 //! screenshot of a preview, a recording's still. The work service keeps
3−//! them (`upload_attachment`), with whoever may comment on the repository
4−//! allowed to add one, and the usercontent origin serves each at
3+//! them (`upload_attachment`), with whoever may comment on the repository,
4+//! or on the work item, allowed to add one, and the usercontent origin serves each at
55 //! `<usercontent>/attachments/<id>` (apps/web/workers/usercontent.ts),
66 //! never the site. An id is 256 random bits, so its address is its
77 //! permission, as a Docs file's is: a private repository's attachment is
2525 /// `attachments/<id>`, with [`AttachmentMeta`].
2626 pub const ATTACHMENT_KEY_PREFIX: &str = "attachments/";
2727
28−/// `upload_attachment`: keep a file to show in Markdown on `repo`, for
29−/// someone who may comment there. `data` is the file, base64-encoded; its
28+/// `upload_attachment`: keep a file to show in Markdown on `repo`, or
29+/// without one on work item `number` of `workspace` (one filed with no
30+/// repository takes files too), for someone who may comment there. `data`
31+/// is the file, base64-encoded; its
3032 /// type is read from its bytes, and a `content_type` that says otherwise
3133 /// is refused. Returns `Outcome<Attachment>`: too large is
3234 /// `FailureCode::TooLarge`, another type `FailureCode::UnsupportedType`.
3335 #[derive(Debug, Serialize, Deserialize)]
3436 pub struct UploadAttachmentArgs {
3537 pub actor: User,
36− pub repo: RepoPath,
38+ #[serde(default, skip_serializing_if = "Option::is_none")]
39+ pub repo: Option<RepoPath>,
40+ /// With `number`, the work item it is for, when `repo` is not given.
41+ #[serde(default, skip_serializing_if = "String::is_empty")]
42+ pub workspace: String,
43+ #[serde(default, skip_serializing_if = "Option::is_none")]
44+ pub number: Option<u32>,
3745 /// The file's name, such as `preview.png`: the image's alt text.
3846 pub name: String,
3947 #[serde(default, skip_serializing_if = "Option::is_none")]
5967 /// As the usercontent origin serves it.
6068 pub content_type: String,
6169 pub name: String,
62− pub repo_id: String,
70+ /// The repository it was put in, or (`item_id`) the work item.
71+ #[serde(default, skip_serializing_if = "Option::is_none")]
72+ pub repo_id: Option<String>,
73+ #[serde(default, skip_serializing_if = "Option::is_none")]
74+ pub item_id: Option<String>,
6375 pub user_id: String,
6476 pub size: u64,
6577 /// Milliseconds since the epoch.
158170 let meta = AttachmentMeta {
159171 content_type: "image/webp".into(),
160172 name: "é".repeat(MAX_ATTACHMENT_NAME_CHARS),
161− repo_id: "repo_01m4z09g22e7ysqpg2ha423cws".into(),
173+ repo_id: Some("repo_01m4z09g22e7ysqpg2ha423cws".into()),
174+ item_id: None,
162175 user_id: "usr_01m4z09g22e7ysqpg2ha423cws".into(),
163176 size: MAX_ATTACHMENT_BYTES as u64,
164177 at: 1_792_627_200_000,
+1−0
11751175 ("react_to_work_item", Scope::WorkWrite),
11761176 // An image for a description or a comment: whoever may comment.
11771177 ("upload_attachment", Scope::WorkWrite),
1178+ ("upload_work_item_attachment", Scope::WorkWrite),
11781179 ("create_commit_comment", Scope::WorkWrite),
11791180 ("update_commit_comment", Scope::WorkWrite),
11801181 ("delete_commit_comment", Scope::WorkWrite),
+4−0
439439 pub items: Vec<MigratedItem>,
440440 /// Where the next page starts; null on the last.
441441 pub next: Option<String>,
442+ /// Whether every issue has its work item's number: until then the map
443+ /// is not whole, and a service catching up on it waits.
444+ #[serde(default)]
445+ pub moved: bool,
442446 }
443447
444448 #[cfg(test)]
+36−7
5151
5252 /// Uploads one file to `repo`: `{ id, name, url, markdown, content_type, size }`.
5353 pub fn upload(client: &Client, repo: &Repo, path: &Path) -> Result<Value> {
54+ let (name, kind, data) = read(path)?;
55+ client.op("upload_attachment").repo(repo).field("name", name).field_opt("content_type", kind).field("data", data).send()
56+}
57+
58+/// Uploads one file to work item `number` of `workspace`, which keeps it:
59+/// one filed with no repository takes files too.
60+pub fn upload_to_item(client: &Client, workspace: &str, number: u64, path: &Path) -> Result<Value> {
61+ let (name, kind, data) = read(path)?;
62+ client
63+ .op("upload_work_item_attachment")
64+ .arg("workspace", workspace)
65+ .arg("number", number)
66+ .field("name", name)
67+ .field_opt("content_type", kind)
68+ .field("data", data)
69+ .send()
70+}
71+
72+/// A file's name, the type its name says and its bytes in base64, once it
73+/// is known to be small enough.
74+fn read(path: &Path) -> Result<(String, Option<&'static str>, String)> {
5475 let shown = path.display();
5576 let size = std::fs::metadata(path)
5677 .map_err(|error| Error::usage(format!("{shown}: {error}")))?
6889 .and_then(|name| name.to_str())
6990 .unwrap_or("image")
7091 .to_owned();
71− client
72− .op("upload_attachment")
73− .repo(repo)
74− .field("name", name)
75− .field_opt("content_type", content_type(path))
76− .field("data", base64::engine::general_purpose::STANDARD.encode(bytes))
77− .send()
92+ Ok((name, content_type(path), base64::engine::general_purpose::STANDARD.encode(bytes)))
7893 }
7994
8095 /// `body` with the Markdown of each of `files` after it, uploaded to
91106 Ok(Some(appended(body.as_deref(), &markdown)))
92107 }
93108
109+/// As [`with_attachments`], with the files kept by work item `number` of
110+/// `workspace`.
111+pub fn with_item_attachments(client: &Client, workspace: &str, number: u64, body: Option<String>, files: &[PathBuf]) -> Result<Option<String>> {
112+ if files.is_empty() {
113+ return Ok(body);
114+ }
115+ let mut markdown = Vec::with_capacity(files.len());
116+ for file in files {
117+ let uploaded = upload_to_item(client, workspace, number, file)?;
118+ markdown.push(s(&uploaded, "markdown").to_owned());
119+ }
120+ Ok(Some(appended(body.as_deref(), &markdown)))
121+}
122+
94123 /// `body`, then a blank line, then each image's Markdown on a line of its own.
95124 pub fn appended(body: Option<&str>, markdown: &[String]) -> String {
96125 let images = markdown.join("\n");
+12−9
263263 args.body.body_file.as_deref(),
264264 args.body.body_file.is_none() && args.attach.is_empty() && prompt::interactive() && !ctx.out.structured(),
265265 )?;
266− let body = if args.attach.is_empty() {
267− body
268− } else {
269− super::attach::with_attachments(&place.client, place.here("--attach")?, body, &args.attach)?
266+ // Images go with a repository it is filed from; without one, the work
267+ // item keeps them, once it is open.
268+ let (body, later) = match &place.repo {
269+ Some(repo) if !args.attach.is_empty() => (super::attach::with_attachments(&place.client, repo, body, &args.attach)?, &[][..]),
270+ _ => (body, &args.attach[..]),
270271 };
271272 let labels: Vec<Value> = args.label.iter().map(|l| Value::String(l.clone())).collect();
272273 let targets: Vec<Value> = args.target.iter().map(|t| Value::String(t.trim().to_owned())).collect();
280281 .field_opt("milestone", args.milestone)
281282 .send()?;
282283 let number = made["number"].as_u64().unwrap_or_default();
284+ let made = if later.is_empty() {
285+ made
286+ } else {
287+ let body = super::attach::with_item_attachments(&place.client, &place.workspace, number, made["body"].as_str().map(str::to_owned), later)?;
288+ place.op("update_work_item").arg("number", number).field_opt("body", body).send()?
289+ };
283290 let made = if args.assignee.is_empty() {
284291 made
285292 } else {
297304 let place = place(ctx, workspace)?;
298305 // Images alone make a comment; without any, the text is asked for.
299306 let body = prompt::body(args.body.body, args.body.body_file.as_deref(), args.attach.is_empty())?;
300− let body = if args.attach.is_empty() {
301− body
302− } else {
303− super::attach::with_attachments(&place.client, place.here("--attach")?, body, &args.attach)?
304− };
307+ let body = super::attach::with_item_attachments(&place.client, &place.workspace, args.number, body, &args.attach)?;
305308 let body = body
306309 .filter(|b| !b.trim().is_empty())
307310 .ok_or_else(|| prompt::needed("The comment", "--body, --body-file or --attach"))?;
+3−3
262262 Mapping { command: "session touch", ops: &["record_session_entries"] },
263263 Mapping { command: "work list", ops: &["list_work_items"] },
264264 Mapping { command: "work view", ops: &["get_work", "get_work_item"] },
265− Mapping { command: "work new", ops: &["open_work_item", "update_work_item", "upload_attachment"] },
266− Mapping { command: "work comment", ops: &["add_work_item_comment", "upload_attachment"] },
265+ Mapping { command: "work new", ops: &["open_work_item", "update_work_item", "upload_attachment", "upload_work_item_attachment"] },
266+ Mapping { command: "work comment", ops: &["add_work_item_comment", "upload_work_item_attachment"] },
267267 Mapping { command: "work edit", ops: &["update_work_item", "get_work_item"] },
268268 Mapping { command: "work close", ops: &["close_work_item", "add_work_item_comment"] },
269269 Mapping { command: "work reopen", ops: &["reopen_work_item", "add_work_item_comment"] },
278278 Mapping { command: "work events", ops: &["list_events"] },
279279 Mapping { command: "work dispatch", ops: &["start_agent_session"] },
280280 Mapping { command: "why", ops: &["why"] },
281− Mapping { command: "attach", ops: &["upload_attachment"] },
281+ Mapping { command: "attach", ops: &["upload_attachment", "upload_work_item_attachment"] },
282282 // Every operation, by name: the table generated from the OpenAPI document.
283283 Mapping {
284284 command: "api",
+18−0
73157315 ],
73167316 },
73177317 Operation {
7318+ id: "upload_work_item_attachment",
7319+ method: "POST",
7320+ path: "/workspaces/{workspace}/work/{number}/attachments",
7321+ tag: "Work items",
7322+ summary: "Upload an attachment to a work item",
7323+ scope: Some("work:write"),
7324+ params: &[
7325+ Param { name: "workspace", location: In::Path, kind: "string", required: true, values: &[] },
7326+ Param { name: "number", location: In::Path, kind: "integer", required: true, values: &[] },
7327+ ],
7328+ body: &[
7329+ Field { name: "content_type", kind: "string", required: false, values: &["image/png", "image/jpeg", "image/gif", "image/webp"] },
7330+ Field { name: "data", kind: "string", required: true, values: &[] },
7331+ Field { name: "name", kind: "string", required: true, values: &[] },
7332+ Field { name: "repo", kind: "string", required: false, values: &[] },
7333+ ],
7334+ },
7335+ Operation {
73187336 id: "create_workspace",
73197337 method: "POST",
73207338 path: "/workspaces",
+1−0
505505 ["delete_work_item_comment", "work:write"],
506506 ["react_to_work_item", "work:write"],
507507 ["upload_attachment", "work:write"],
508+ ["upload_work_item_attachment", "work:write"],
508509 ["create_commit_comment", "work:write"],
509510 ["update_commit_comment", "work:write"],
510511 ["delete_commit_comment", "work:write"],
+1−1
33 version = "0.1.0"
44 edition.workspace = true
55 license.workspace = true
6−description = "Issues, pull requests, reviews, checks and sessions."
6+description = "Work items, pull requests, reviews, checks and sessions."
77
88 [lib]
99 crate-type = ["cdylib"]
+25−0
1+-- Attachments on work items (attachments.rs): a work item filed without a
2+-- repository takes files too, kept by the work item (`item_id`, with its
3+-- `workspace`, so a deleted workspace's go with it) instead of a
4+-- repository. Each row names one or the other.
5+CREATE TABLE attachments_new (
6+ id TEXT PRIMARY KEY,
7+ repo_id TEXT,
8+ item_id TEXT,
9+ workspace TEXT,
10+ user_id TEXT NOT NULL,
11+ name TEXT NOT NULL,
12+ content_type TEXT NOT NULL,
13+ size INTEGER NOT NULL,
14+ created_at TEXT NOT NULL,
15+ CHECK (repo_id IS NOT NULL OR item_id IS NOT NULL)
16+);
17+
18+INSERT INTO attachments_new (id, repo_id, user_id, name, content_type, size, created_at)
19+SELECT id, repo_id, user_id, name, content_type, size, created_at FROM attachments;
20+
21+DROP TABLE attachments;
22+ALTER TABLE attachments_new RENAME TO attachments;
23+
24+CREATE INDEX attachments_by_repo ON attachments (repo_id);
25+CREATE INDEX attachments_by_workspace ON attachments (workspace) WHERE workspace IS NOT NULL;
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…

Comments

Sign in to comment.