Skip to content

Commit 694e0e3

Browse files
committed
feat: add b2b --id flag
1 parent 869a88a commit 694e0e3

File tree

5 files changed

+99
-74
lines changed

5 files changed

+99
-74
lines changed

README.md

Lines changed: 14 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ shipit config generate
5757
shipit config show
5858

5959
# 3. Ship it from the root of your project. See the command docs below for more options
60-
shipit b2b develop main --dry-run
60+
shipit b2b develop main --dryrun
6161
```
6262

6363
---
@@ -74,16 +74,17 @@ shipit b2b develop main --dry-run
7474
### `b2b` — Branch to Branch
7575

7676
```
77-
shipit b2b <source> <target> [--ai] [--dryrun] [--dir <path>]
77+
shipit b2b <source> <target> [--ai] [--dryrun] [--dir <path>] [--id <project-id>]
7878
```
7979

80-
| Argument / Flag | Description |
81-
|-----------------|-------------|
82-
| `source` | Branch with new commits (e.g. `develop`) |
83-
| `target` | Destination branch (e.g. `main`) |
84-
| `--ai` | Enable Ollama LLM to generate categorized release notes |
85-
| `--dryrun` | Preview the merge request description without creating it |
86-
| `--dir <path>` | Path to the git repository (defaults to current directory) |
80+
| Argument / Flag | Description |
81+
|----------------------|-------------|
82+
| `source` | Branch with new commits (e.g. `develop`) |
83+
| `target` | Destination branch (e.g. `main`) |
84+
| `--ai` | Enable Ollama LLM to generate categorized release notes |
85+
| `--dryrun` | Preview the merge request description without creating it |
86+
| `--dir <path>` | Path to the git repository (defaults to current directory) |
87+
| `--id <project-id>` | GitLab numeric project ID required to open a merge request |
8788

8889
**What happens:**
8990

@@ -95,16 +96,16 @@ shipit b2b <source> <target> [--ai] [--dryrun] [--dir <path>]
9596

9697
```bash
9798
# Basic — raw commit list as MR description
98-
shipit b2b develop main
99+
shipit b2b develop main --id 12345678
99100

100101
# With AI-generated release notes
101-
shipit b2b develop main --ai
102+
shipit b2b develop main --id 12345678 --ai
102103

103104
# Dry run — see the description without creating the MR
104-
shipit b2b develop main --ai --dryrun
105+
shipit b2b develop main --id 12345678 --ai --dryrun
105106

106107
# Point at a repo outside the current directory
107-
shipit b2b develop main --dir /path/to/repo
108+
shipit b2b develop main --id 12345678 --ai --dir /path/to/repo
108109
```
109110

110111
---

src/cli.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ pub enum Commands {
1717
dryrun: bool,
1818
#[arg(long)]
1919
dir: Option<String>,
20+
#[arg(long, required_unless_present = "dryrun", help = "GitLab/GitHub Project ID")]
21+
id: Option<u64>,
2022
},
2123
Config {
2224
#[command(subcommand)]

src/commands/b2b.rs

Lines changed: 18 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -1,53 +1,17 @@
11
use std::env;
22

33
use git2::Repository;
4-
use gitlab::api::{projects, AsyncQuery};
5-
use reqwest::Client;
6-
use serde_json::json;
74

85
use crate::context::Context;
96
use crate::error::ShipItError;
10-
use crate::settings::OllamaSettings;
11-
12-
async fn summarize_with_ollama(text: String, ollama: &OllamaSettings) -> Result<String, ShipItError> {
13-
let client = Client::new();
14-
15-
let prompt = format!(
16-
"You are a technical writer tasked with creating organized and concise release notes. Categorize the following comma separated list of commit titles followed by their commit ids into markdown formatted subheadings. The heading cateogries are new features, bug fixes, infrastructure, and docs. If a category has no content, exclude it from the output. Do not format or alter the commit messages in any other way. Do not wrap the body of your result in markdown syntax highlighting ticks.\n\n{}",
17-
text
18-
);
19-
20-
let url = format!("http://{}:{}{}", ollama.domain, ollama.port, ollama.endpoint);
21-
22-
let response = client.post(&url)
23-
.json(&json!({
24-
"model": ollama.model,
25-
"prompt": prompt,
26-
"stream": false,
27-
"options": {
28-
"temperature": ollama.options.temperature,
29-
"top_p": ollama.options.top_p,
30-
"seed": ollama.options.seed
31-
}
32-
}))
33-
.send()
34-
.await.map_err(|e| ShipItError::Http(e))?
35-
.json::<serde_json::Value>()
36-
.await.map_err(|e| ShipItError::Http(e))?;
37-
38-
// Extract the response string from the JSON
39-
let summary = response["response"]
40-
.as_str()
41-
.ok_or_else(|| ShipItError::Error("Failed to parse Ollama response!".to_string()))?;
42-
43-
Ok(summary.to_string())
44-
}
7+
use crate::common::{open_gitlab_mr, summarize_with_ollama};
458

469
pub async fn branch_to_branch(
4710
ctx: &Context,
4811
args_source: String,
4912
args_target: String,
5013
args_dir: Option<String>,
14+
args_id: Option<u64>,
5115
) -> Result<(), ShipItError> {
5216
let dir = match args_dir {
5317
Some(path) => std::path::PathBuf::from(path),
@@ -115,10 +79,10 @@ pub async fn branch_to_branch(
11579
}
11680

11781
// ask a local llm to summarize these commit messages
118-
// this is an async operation that needs to be blocked
119-
// downstream tasks cant function without the results
12082
let mut summary = if ctx.settings.shipit.ai {
121-
let result = summarize_with_ollama(description, &ctx.settings.ollama).await?;
83+
let result = summarize_with_ollama(
84+
&description, &ctx.settings.ollama
85+
).await.or_else(|_e| Err(ShipItError::Error("Failed to summarize with Ollama!".to_string())))?;
12286
println!("The merge request description is:\n\n{}", result);
12387
result
12488
} else {
@@ -131,24 +95,19 @@ pub async fn branch_to_branch(
13195
return Ok(());
13296
}
13397

134-
// open a gitlab mr
135-
let token = ctx.settings.gitlab.token.as_deref()
136-
.ok_or_else(|| ShipItError::Error("GitLab token not configured. Set gitlab.token in your shipit config.".to_string()))?;
137-
138-
let client = gitlab::Gitlab::builder(&ctx.settings.gitlab.domain, token).build_async().await.map_err(|e| ShipItError::Gitlab(e))?;
139-
140-
let create_mr = projects::merge_requests::CreateMergeRequest::builder()
141-
.project(79411719)
142-
.source_branch(&args_source)
143-
.target_branch(&args_target)
144-
.title(format!("{} to {}", &args_source, &args_target))
145-
.description(&summary)
146-
.remove_source_branch(true)
147-
.build().map_err(|_e| ShipItError::Error("Failed to build a Gitlab MR!".to_string()))?;
148-
149-
let merge_request: serde_json::Value = create_mr.query_async(&client).await.map_err(|_e| ShipItError::Error("Failed to create a Gitlab merge request!".to_string()))?;
150-
151-
println!("\n\nThe merge request is available at:\n\n{}", merge_request["web_url"]);
98+
// open an mr
99+
if args_id.is_some() {
100+
let project_id = args_id.as_ref().unwrap();
101+
let token = ctx.settings.gitlab.token.as_deref()
102+
.ok_or_else(|| ShipItError::Error("GitLab token not configured. Set gitlab.token in your shipit config.".to_string()))?;
103+
let mr_url = open_gitlab_mr(
104+
&args_source, &args_target, &ctx.settings.gitlab.domain,
105+
token, project_id, &summary
106+
).await.or_else(|_e| Err(ShipItError::Error("Failed to open a Gitlab MR!".to_string())))?;
107+
println!("\n\nThe merge request is available at:\n\n{}", mr_url["web_url"]);
108+
} else {
109+
return Err(ShipItError::Error("Unable to open a Gitlab MR without a project id specified with '--id'".to_string()));
110+
}
152111

153112
Ok(())
154113
}

src/common.rs

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
use gitlab::api::{projects, AsyncQuery};
2+
use gitlab::Gitlab;
3+
use reqwest::Client;
4+
use serde_json::json;
5+
6+
use crate::error::ShipItError;
7+
use crate::settings::OllamaSettings;
8+
9+
10+
pub(crate) async fn open_gitlab_mr(
11+
source: &str, target: &str, domain: &str, token: &str,
12+
project_id: &u64, description: &str
13+
) -> Result<serde_json::Value, ShipItError> {
14+
let client = Gitlab::builder(domain, token).build_async().await.map_err(|e| ShipItError::Gitlab(e))?;
15+
16+
let create_mr = projects::merge_requests::CreateMergeRequest::builder()
17+
.project(*project_id)
18+
.source_branch(source)
19+
.target_branch(target)
20+
.title(format!("{} to {}", source, target))
21+
.description(description)
22+
.remove_source_branch(true)
23+
.build().map_err(|_e| ShipItError::Error("Failed to build a Gitlab MR!".to_string()))?;
24+
25+
let merge_request: serde_json::Value = create_mr.query_async(&client).await.map_err(|_e| ShipItError::Error("Failed to create a Gitlab merge request!".to_string()))?;
26+
27+
Ok(merge_request)
28+
}
29+
30+
31+
pub(crate) async fn summarize_with_ollama(text: &str, ollama: &OllamaSettings) -> Result<String, ShipItError> {
32+
let client = Client::new();
33+
34+
let prompt = format!(
35+
"You are a technical writer tasked with creating organized and concise release notes. Categorize the following comma separated list of commit titles followed by their commit ids into markdown formatted subheadings. The heading cateogries are new features, bug fixes, infrastructure, and docs. If a category has no content, exclude it from the output. Do not format or alter the commit messages in any other way. Do not wrap the body of your result in markdown syntax highlighting ticks.\n\n{}",
36+
text
37+
);
38+
39+
let url = format!("http://{}:{}{}", ollama.domain, ollama.port, ollama.endpoint);
40+
41+
let response = client.post(&url)
42+
.json(&json!({
43+
"model": ollama.model,
44+
"prompt": prompt,
45+
"stream": false,
46+
"options": {
47+
"temperature": ollama.options.temperature,
48+
"top_p": ollama.options.top_p,
49+
"seed": ollama.options.seed
50+
}
51+
}))
52+
.send()
53+
.await.map_err(|e| ShipItError::Http(e))?
54+
.json::<serde_json::Value>()
55+
.await.map_err(|e| ShipItError::Http(e))?;
56+
57+
let summary = response["response"]
58+
.as_str()
59+
.ok_or_else(|| ShipItError::Error("Failed to parse Ollama response!".to_string()))?;
60+
61+
Ok(summary.to_string())
62+
}

src/main.rs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
mod cli;
22
mod commands;
3+
mod common;
34
mod context;
45
mod error;
56
mod settings;
@@ -23,8 +24,8 @@ async fn main() -> Result<(), ShipItError> {
2324

2425
let ctx = Context::from_cli(&args).map_err(|_e| ShipItError::Error("Failed to parse CLI context!".to_string()))?;
2526
match args.command {
26-
cli::Commands::B2b { source, target, dir, .. } => {
27-
commands::b2b::branch_to_branch(&ctx, source, target, dir).await?;
27+
cli::Commands::B2b { source, target, dir, id, .. } => {
28+
commands::b2b::branch_to_branch(&ctx, source, target, dir, id).await?;
2829
}
2930
cli::Commands::Config { .. } => unreachable!(),
3031
}

0 commit comments

Comments
 (0)