Compare commits
13 Commits
fix-git-ht
...
v0.132.2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6d6d6c69db | ||
|
|
81cb4ee157 | ||
|
|
4db7841fb1 | ||
|
|
3337e5f2f7 | ||
|
|
58dd76efaf | ||
|
|
bfc2057376 | ||
|
|
22d07862fd | ||
|
|
056739c1a2 | ||
|
|
63f208b822 | ||
|
|
877666bd03 | ||
|
|
cabfb69329 | ||
|
|
d97960fd8c | ||
|
|
68e0bea835 |
4
.github/workflows/ci.yml
vendored
4
.github/workflows/ci.yml
vendored
@@ -205,6 +205,8 @@ jobs:
|
||||
echo "invalid release tag ${GITHUB_REF_NAME}. expected ${expected_tag_name}"
|
||||
exit 1
|
||||
fi
|
||||
mkdir -p target/
|
||||
script/draft-release-notes "$version" "$channel" > target/release-notes.md
|
||||
|
||||
- name: Generate license file
|
||||
run: script/generate-licenses
|
||||
@@ -248,7 +250,7 @@ jobs:
|
||||
target/aarch64-apple-darwin/release/Zed-aarch64.dmg
|
||||
target/x86_64-apple-darwin/release/Zed-x86_64.dmg
|
||||
target/release/Zed.dmg
|
||||
body: ""
|
||||
body_path: target/release-notes.md
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
|
||||
4
Cargo.lock
generated
4
Cargo.lock
generated
@@ -9736,7 +9736,6 @@ dependencies = [
|
||||
"file_icons",
|
||||
"fuzzy",
|
||||
"gpui",
|
||||
"itertools 0.11.0",
|
||||
"language",
|
||||
"picker",
|
||||
"project",
|
||||
@@ -9841,7 +9840,6 @@ dependencies = [
|
||||
"serde_json",
|
||||
"settings",
|
||||
"shellexpand",
|
||||
"shlex",
|
||||
"smol",
|
||||
"task",
|
||||
"terminal",
|
||||
@@ -12522,7 +12520,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "zed"
|
||||
version = "0.132.0"
|
||||
version = "0.132.2"
|
||||
dependencies = [
|
||||
"activity_indicator",
|
||||
"anyhow",
|
||||
|
||||
@@ -298,7 +298,6 @@ serde_json_lenient = { version = "0.1", features = [
|
||||
] }
|
||||
serde_repr = "0.1"
|
||||
sha2 = "0.10"
|
||||
shlex = "1.3"
|
||||
shellexpand = "2.1.0"
|
||||
smallvec = { version = "1.6", features = ["union"] }
|
||||
smol = "1.2"
|
||||
|
||||
@@ -227,7 +227,7 @@ impl ExtensionImports for WasmState {
|
||||
"lsp" => {
|
||||
let settings = key
|
||||
.and_then(|key| {
|
||||
ProjectSettings::get_global(cx)
|
||||
ProjectSettings::get(location, cx)
|
||||
.lsp
|
||||
.get(&Arc::<str>::from(key))
|
||||
})
|
||||
|
||||
@@ -337,7 +337,6 @@ struct MacWindowState {
|
||||
handle: AnyWindowHandle,
|
||||
executor: ForegroundExecutor,
|
||||
native_window: id,
|
||||
native_window_was_closed: bool,
|
||||
native_view: NonNull<Object>,
|
||||
display_link: Option<DisplayLink>,
|
||||
renderer: renderer::Renderer,
|
||||
@@ -605,6 +604,10 @@ impl MacWindow {
|
||||
registerForDraggedTypes:
|
||||
NSArray::arrayWithObject(nil, NSFilenamesPboardType)
|
||||
];
|
||||
let () = msg_send![
|
||||
native_window,
|
||||
setReleasedWhenClosed: NO
|
||||
];
|
||||
|
||||
let native_view: id = msg_send![VIEW_CLASS, alloc];
|
||||
let native_view = NSView::init(native_view);
|
||||
@@ -622,7 +625,6 @@ impl MacWindow {
|
||||
handle,
|
||||
executor,
|
||||
native_window,
|
||||
native_window_was_closed: false,
|
||||
native_view: NonNull::new_unchecked(native_view),
|
||||
display_link: None,
|
||||
renderer: renderer::new_renderer(
|
||||
@@ -770,19 +772,17 @@ impl Drop for MacWindow {
|
||||
this.renderer.destroy();
|
||||
let window = this.native_window;
|
||||
this.display_link.take();
|
||||
if !this.native_window_was_closed {
|
||||
unsafe {
|
||||
this.native_window.setDelegate_(nil);
|
||||
}
|
||||
|
||||
this.executor
|
||||
.spawn(async move {
|
||||
unsafe {
|
||||
window.close();
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
unsafe {
|
||||
this.native_window.setDelegate_(nil);
|
||||
}
|
||||
this.executor
|
||||
.spawn(async move {
|
||||
unsafe {
|
||||
window.close();
|
||||
window.autorelease();
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1592,7 +1592,6 @@ extern "C" fn close_window(this: &Object, _: Sel) {
|
||||
let close_callback = {
|
||||
let window_state = get_window_state(this);
|
||||
let mut lock = window_state.as_ref().lock();
|
||||
lock.native_window_was_closed = true;
|
||||
lock.close_callback.take()
|
||||
};
|
||||
|
||||
|
||||
@@ -15,9 +15,9 @@ pub trait ContextProvider: Send + Sync {
|
||||
/// Builds a specific context to be placed on top of the basic one (replacing all conflicting entries) and to be used for task resolving later.
|
||||
fn build_context(
|
||||
&self,
|
||||
_: Option<&Path>,
|
||||
_: &Location,
|
||||
_: &mut AppContext,
|
||||
_worktree_abs_path: Option<&Path>,
|
||||
_location: &Location,
|
||||
_cx: &mut AppContext,
|
||||
) -> Result<TaskVariables> {
|
||||
Ok(TaskVariables::default())
|
||||
}
|
||||
|
||||
@@ -85,13 +85,7 @@ pub fn init(
|
||||
config.name.clone(),
|
||||
config.grammar.clone(),
|
||||
config.matcher.clone(),
|
||||
move || {
|
||||
Ok((
|
||||
config.clone(),
|
||||
load_queries($name),
|
||||
Some(Arc::new(language::BasicContextProvider)),
|
||||
))
|
||||
},
|
||||
move || Ok((config.clone(), load_queries($name), None)),
|
||||
);
|
||||
};
|
||||
($name:literal, $adapters:expr) => {
|
||||
@@ -105,13 +99,7 @@ pub fn init(
|
||||
config.name.clone(),
|
||||
config.grammar.clone(),
|
||||
config.matcher.clone(),
|
||||
move || {
|
||||
Ok((
|
||||
config.clone(),
|
||||
load_queries($name),
|
||||
Some(Arc::new(language::BasicContextProvider)),
|
||||
))
|
||||
},
|
||||
move || Ok((config.clone(), load_queries($name), None)),
|
||||
);
|
||||
};
|
||||
($name:literal, $adapters:expr, $context_provider:expr) => {
|
||||
|
||||
@@ -186,13 +186,7 @@ pub(super) fn python_task_context() -> ContextProviderWithTasks {
|
||||
TaskTemplate {
|
||||
label: "execute selection".to_owned(),
|
||||
command: "python3".to_owned(),
|
||||
args: vec![
|
||||
"-c".to_owned(),
|
||||
format!(
|
||||
"exec(r'''{}''')",
|
||||
VariableName::SelectedText.template_value()
|
||||
),
|
||||
],
|
||||
args: vec!["-c".to_owned(), VariableName::SelectedText.template_value()],
|
||||
ignore_previously_resolved: true,
|
||||
..TaskTemplate::default()
|
||||
},
|
||||
|
||||
@@ -421,13 +421,6 @@ impl ContextProvider for RustContextProvider {
|
||||
}
|
||||
|
||||
fn human_readable_package_name(package_directory: &Path) -> Option<String> {
|
||||
fn split_off_suffix(input: &str, suffix_start: char) -> &str {
|
||||
match input.rsplit_once(suffix_start) {
|
||||
Some((without_suffix, _)) => without_suffix,
|
||||
None => input,
|
||||
}
|
||||
}
|
||||
|
||||
let pkgid = String::from_utf8(
|
||||
std::process::Command::new("cargo")
|
||||
.current_dir(package_directory)
|
||||
@@ -437,19 +430,40 @@ fn human_readable_package_name(package_directory: &Path) -> Option<String> {
|
||||
.stdout,
|
||||
)
|
||||
.ok()?;
|
||||
// For providing local `cargo check -p $pkgid` task, we do not need most of the information we have returned.
|
||||
// Output example in the root of Zed project:
|
||||
// ```bash
|
||||
// ❯ cargo pkgid zed
|
||||
// path+file:///absolute/path/to/project/zed/crates/zed#0.131.0
|
||||
// ```
|
||||
// Extrarct the package name from the output according to the spec:
|
||||
// https://doc.rust-lang.org/cargo/reference/pkgid-spec.html#specification-grammar
|
||||
let mut package_name = pkgid.trim();
|
||||
package_name = split_off_suffix(package_name, '#');
|
||||
package_name = split_off_suffix(package_name, '?');
|
||||
let (_, package_name) = package_name.rsplit_once('/')?;
|
||||
Some(package_name.to_string())
|
||||
Some(package_name_from_pkgid(&pkgid)?.to_owned())
|
||||
}
|
||||
|
||||
// For providing local `cargo check -p $pkgid` task, we do not need most of the information we have returned.
|
||||
// Output example in the root of Zed project:
|
||||
// ```bash
|
||||
// ❯ cargo pkgid zed
|
||||
// path+file:///absolute/path/to/project/zed/crates/zed#0.131.0
|
||||
// ```
|
||||
// Another variant, if a project has a custom package name or hyphen in the name:
|
||||
// ```
|
||||
// path+file:///absolute/path/to/project/custom-package#my-custom-package@0.1.0
|
||||
// ```
|
||||
//
|
||||
// Extracts the package name from the output according to the spec:
|
||||
// https://doc.rust-lang.org/cargo/reference/pkgid-spec.html#specification-grammar
|
||||
fn package_name_from_pkgid(pkgid: &str) -> Option<&str> {
|
||||
fn split_off_suffix(input: &str, suffix_start: char) -> &str {
|
||||
match input.rsplit_once(suffix_start) {
|
||||
Some((without_suffix, _)) => without_suffix,
|
||||
None => input,
|
||||
}
|
||||
}
|
||||
|
||||
let (version_prefix, version_suffix) = pkgid.trim().rsplit_once('#')?;
|
||||
let package_name = match version_suffix.rsplit_once('@') {
|
||||
Some((custom_package_name, _version)) => custom_package_name,
|
||||
None => {
|
||||
let host_and_path = split_off_suffix(version_prefix, '?');
|
||||
let (_, package_name) = host_and_path.rsplit_once('/')?;
|
||||
package_name
|
||||
}
|
||||
};
|
||||
Some(package_name)
|
||||
}
|
||||
|
||||
async fn get_cached_server_binary(container_dir: PathBuf) -> Option<LanguageServerBinary> {
|
||||
@@ -750,4 +764,20 @@ mod tests {
|
||||
buffer
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_package_name_from_pkgid() {
|
||||
for (input, expected) in [
|
||||
(
|
||||
"path+file:///absolute/path/to/project/zed/crates/zed#0.131.0",
|
||||
"zed",
|
||||
),
|
||||
(
|
||||
"path+file:///absolute/path/to/project/custom-package#my-custom-package@0.1.0",
|
||||
"my-custom-package",
|
||||
),
|
||||
] {
|
||||
assert_eq!(package_name_from_pkgid(input), Some(expected));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -219,6 +219,13 @@ impl Inventory {
|
||||
.iter()
|
||||
.rev()
|
||||
.filter(|(_, task)| !task.original_task().ignore_previously_resolved)
|
||||
.filter(|(task_kind, _)| {
|
||||
if matches!(task_kind, TaskSourceKind::Language { .. }) {
|
||||
Some(task_kind) == task_source_kind.as_ref()
|
||||
} else {
|
||||
true
|
||||
}
|
||||
})
|
||||
.fold(
|
||||
HashMap::default(),
|
||||
|mut tasks, (task_source_kind, resolved_task)| {
|
||||
|
||||
@@ -4,9 +4,10 @@ use gpui::{AnyWindowHandle, Context, Entity, Model, ModelContext, WeakModel};
|
||||
use settings::Settings;
|
||||
use smol::channel::bounded;
|
||||
use std::path::{Path, PathBuf};
|
||||
use task::SpawnInTerminal;
|
||||
use terminal::{
|
||||
terminal_settings::{self, Shell, TerminalSettings, VenvSettingsContent},
|
||||
SpawnTask, TaskState, TaskStatus, Terminal, TerminalBuilder,
|
||||
TaskState, TaskStatus, Terminal, TerminalBuilder,
|
||||
};
|
||||
use util::ResultExt;
|
||||
|
||||
@@ -21,7 +22,7 @@ impl Project {
|
||||
pub fn create_terminal(
|
||||
&mut self,
|
||||
working_directory: Option<PathBuf>,
|
||||
spawn_task: Option<SpawnTask>,
|
||||
spawn_task: Option<SpawnInTerminal>,
|
||||
window: AnyWindowHandle,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) -> anyhow::Result<Model<Terminal>> {
|
||||
@@ -55,14 +56,7 @@ impl Project {
|
||||
id: spawn_task.id,
|
||||
full_label: spawn_task.full_label,
|
||||
label: spawn_task.label,
|
||||
command_label: spawn_task.args.iter().fold(
|
||||
spawn_task.command.clone(),
|
||||
|mut command_label, new_arg| {
|
||||
command_label.push(' ');
|
||||
command_label.push_str(new_arg);
|
||||
command_label
|
||||
},
|
||||
),
|
||||
command_label: spawn_task.command_label,
|
||||
status: TaskStatus::Running,
|
||||
completion_rx,
|
||||
}),
|
||||
|
||||
@@ -31,8 +31,11 @@ pub struct SpawnInTerminal {
|
||||
pub label: String,
|
||||
/// Executable command to spawn.
|
||||
pub command: String,
|
||||
/// Arguments to the command.
|
||||
/// Arguments to the command, potentially unsubstituted,
|
||||
/// to let the shell that spawns the command to do the substitution, if needed.
|
||||
pub args: Vec<String>,
|
||||
/// A human-readable label, containing command and all of its arguments, joined and substituted.
|
||||
pub command_label: String,
|
||||
/// Current working directory to spawn the command into.
|
||||
pub cwd: Option<PathBuf>,
|
||||
/// Env overrides for the command, will be appended to the terminal's environment from the settings.
|
||||
@@ -75,6 +78,14 @@ impl ResolvedTask {
|
||||
pub fn substituted_variables(&self) -> &HashSet<VariableName> {
|
||||
&self.substituted_variables
|
||||
}
|
||||
|
||||
/// A human-readable label to display in the UI.
|
||||
pub fn display_label(&self) -> &str {
|
||||
self.resolved
|
||||
.as_ref()
|
||||
.map(|resolved| resolved.label.as_str())
|
||||
.unwrap_or_else(|| self.resolved_label.as_str())
|
||||
}
|
||||
}
|
||||
|
||||
/// Variables, available for use in [`TaskContext`] when a Zed's [`TaskTemplate`] gets resolved into a [`ResolvedTask`].
|
||||
|
||||
@@ -156,7 +156,7 @@ impl TaskTemplate {
|
||||
&variable_names,
|
||||
&mut substituted_variables,
|
||||
)?;
|
||||
let args = substitute_all_template_variables_in_vec(
|
||||
let args_with_substitutions = substitute_all_template_variables_in_vec(
|
||||
&self.args,
|
||||
&task_variables,
|
||||
&variable_names,
|
||||
@@ -187,8 +187,16 @@ impl TaskTemplate {
|
||||
cwd,
|
||||
full_label,
|
||||
label: human_readable_label,
|
||||
command_label: args_with_substitutions.iter().fold(
|
||||
command.clone(),
|
||||
|mut command_label, arg| {
|
||||
command_label.push(' ');
|
||||
command_label.push_str(arg);
|
||||
command_label
|
||||
},
|
||||
),
|
||||
command,
|
||||
args,
|
||||
args: self.args.clone(),
|
||||
env,
|
||||
use_new_terminal: self.use_new_terminal,
|
||||
allow_concurrent_runs: self.allow_concurrent_runs,
|
||||
@@ -524,11 +532,16 @@ mod tests {
|
||||
assert_eq!(
|
||||
spawn_in_terminal.args,
|
||||
&[
|
||||
"arg1 test_selected_text",
|
||||
"arg2 5678",
|
||||
&format!("arg3 {long_value}")
|
||||
"arg1 $ZED_SELECTED_TEXT",
|
||||
"arg2 $ZED_COLUMN",
|
||||
"arg3 $ZED_SYMBOL",
|
||||
],
|
||||
"Args should be substituted with variables and those should not be shortened"
|
||||
"Args should not be substituted with variables"
|
||||
);
|
||||
assert_eq!(
|
||||
spawn_in_terminal.command_label,
|
||||
format!("{} arg1 test_selected_text arg2 5678 arg3 {long_value}", spawn_in_terminal.command),
|
||||
"Command label args should be substituted with variables and those should not be shortened"
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
|
||||
@@ -25,7 +25,6 @@ util.workspace = true
|
||||
terminal.workspace = true
|
||||
workspace.workspace = true
|
||||
language.workspace = true
|
||||
itertools.workspace = true
|
||||
|
||||
|
||||
[dev-dependencies]
|
||||
|
||||
@@ -168,8 +168,8 @@ fn task_context(workspace: &Workspace, cx: &mut WindowContext<'_>) -> TaskContex
|
||||
let language_context_provider = buffer
|
||||
.read(cx)
|
||||
.language()
|
||||
.and_then(|language| language.context_provider())?;
|
||||
|
||||
.and_then(|language| language.context_provider())
|
||||
.unwrap_or_else(|| Arc::new(BasicContextProvider));
|
||||
let selection_range = selection.range();
|
||||
let start = editor_snapshot
|
||||
.display_snapshot
|
||||
@@ -470,6 +470,7 @@ mod tests {
|
||||
pub(crate) fn init_test(cx: &mut TestAppContext) -> Arc<AppState> {
|
||||
cx.update(|cx| {
|
||||
let state = AppState::test(cx);
|
||||
file_icons::init((), cx);
|
||||
language::init(cx);
|
||||
crate::init(cx);
|
||||
editor::init(cx);
|
||||
|
||||
@@ -306,7 +306,30 @@ impl PickerDelegate for TasksModalDelegate {
|
||||
) -> Option<Self::ListItem> {
|
||||
let candidates = self.candidates.as_ref()?;
|
||||
let hit = &self.matches[ix];
|
||||
let (source_kind, _) = &candidates.get(hit.candidate_id)?;
|
||||
let (source_kind, resolved_task) = &candidates.get(hit.candidate_id)?;
|
||||
let template = resolved_task.original_task();
|
||||
let display_label = resolved_task.display_label();
|
||||
|
||||
let mut tooltip_label_text = if display_label != &template.label {
|
||||
resolved_task.resolved_label.clone()
|
||||
} else {
|
||||
String::new()
|
||||
};
|
||||
if let Some(resolved) = resolved_task.resolved.as_ref() {
|
||||
if resolved.command_label != display_label
|
||||
&& resolved.command_label != resolved_task.resolved_label
|
||||
{
|
||||
if !tooltip_label_text.trim().is_empty() {
|
||||
tooltip_label_text.push('\n');
|
||||
}
|
||||
tooltip_label_text.push_str(&resolved.command_label);
|
||||
}
|
||||
}
|
||||
let tooltip_label = if tooltip_label_text.trim().is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(Tooltip::text(tooltip_label_text, cx))
|
||||
};
|
||||
|
||||
let highlighted_location = HighlightedText {
|
||||
text: hit.string.clone(),
|
||||
@@ -325,6 +348,9 @@ impl PickerDelegate for TasksModalDelegate {
|
||||
ListItem::new(SharedString::from(format!("tasks-modal-{ix}")))
|
||||
.inset(true)
|
||||
.spacing(ListItemSpacing::Sparse)
|
||||
.when_some(tooltip_label, |list_item, item_label| {
|
||||
list_item.tooltip(move |_| item_label.clone())
|
||||
})
|
||||
.map(|item| {
|
||||
let item = if matches!(source_kind, TaskSourceKind::UserInput)
|
||||
|| Some(ix) <= self.last_used_candidate_index
|
||||
@@ -368,18 +394,10 @@ impl PickerDelegate for TasksModalDelegate {
|
||||
}
|
||||
|
||||
fn selected_as_query(&self) -> Option<String> {
|
||||
use itertools::intersperse;
|
||||
let task_index = self.matches.get(self.selected_index())?.candidate_id;
|
||||
let tasks = self.candidates.as_ref()?;
|
||||
let (_, task) = tasks.get(task_index)?;
|
||||
task.resolved.as_ref().map(|spawn_in_terminal| {
|
||||
let mut command = spawn_in_terminal.command.clone();
|
||||
if !spawn_in_terminal.args.is_empty() {
|
||||
command.push(' ');
|
||||
command.extend(intersperse(spawn_in_terminal.args.clone(), " ".to_string()));
|
||||
}
|
||||
command
|
||||
})
|
||||
Some(task.resolved.as_ref()?.command_label.clone())
|
||||
}
|
||||
|
||||
fn confirm_input(&mut self, omit_history_entry: bool, cx: &mut ViewContext<Picker<Self>>) {
|
||||
@@ -405,17 +423,23 @@ impl PickerDelegate for TasksModalDelegate {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::{path::PathBuf, sync::Arc};
|
||||
|
||||
use editor::Editor;
|
||||
use gpui::{TestAppContext, VisualTestContext};
|
||||
use language::{ContextProviderWithTasks, Language, LanguageConfig, LanguageMatcher, Point};
|
||||
use project::{FakeFs, Project};
|
||||
use serde_json::json;
|
||||
use task::TaskTemplates;
|
||||
use workspace::CloseInactiveTabsAndPanes;
|
||||
|
||||
use crate::modal::Spawn;
|
||||
use crate::{modal::Spawn, tests::init_test};
|
||||
|
||||
use super::*;
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_spawn_tasks_modal_query_reuse(cx: &mut TestAppContext) {
|
||||
crate::tests::init_test(cx);
|
||||
init_test(cx);
|
||||
let fs = FakeFs::new(cx.executor());
|
||||
fs.insert_tree(
|
||||
"/dir",
|
||||
@@ -561,6 +585,294 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_basic_context_for_simple_files(cx: &mut TestAppContext) {
|
||||
init_test(cx);
|
||||
let fs = FakeFs::new(cx.executor());
|
||||
fs.insert_tree(
|
||||
"/dir",
|
||||
json!({
|
||||
".zed": {
|
||||
"tasks.json": r#"[
|
||||
{
|
||||
"label": "hello from $ZED_FILE:$ZED_ROW:$ZED_COLUMN",
|
||||
"command": "echo",
|
||||
"args": ["hello", "from", "$ZED_FILE", ":", "$ZED_ROW", ":", "$ZED_COLUMN"]
|
||||
},
|
||||
{
|
||||
"label": "opened now: $ZED_WORKTREE_ROOT",
|
||||
"command": "echo",
|
||||
"args": ["opened", "now:", "$ZED_WORKTREE_ROOT"]
|
||||
}
|
||||
]"#,
|
||||
},
|
||||
"file_without_extension": "aaaaaaaaaaaaaaaaaaaa\naaaaaaaaaaaaaaaaaa",
|
||||
"file_with.odd_extension": "b",
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
|
||||
let project = Project::test(fs, ["/dir".as_ref()], cx).await;
|
||||
let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
|
||||
|
||||
let tasks_picker = open_spawn_tasks(&workspace, cx);
|
||||
assert_eq!(
|
||||
task_names(&tasks_picker, cx),
|
||||
Vec::<String>::new(),
|
||||
"Should list no file or worktree context-dependent when no file is open"
|
||||
);
|
||||
tasks_picker.update(cx, |_, cx| {
|
||||
cx.emit(DismissEvent);
|
||||
});
|
||||
drop(tasks_picker);
|
||||
cx.executor().run_until_parked();
|
||||
|
||||
let _ = workspace
|
||||
.update(cx, |workspace, cx| {
|
||||
workspace.open_abs_path(PathBuf::from("/dir/file_with.odd_extension"), true, cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
cx.executor().run_until_parked();
|
||||
let tasks_picker = open_spawn_tasks(&workspace, cx);
|
||||
assert_eq!(
|
||||
task_names(&tasks_picker, cx),
|
||||
vec![
|
||||
"hello from …th.odd_extension:1:1".to_string(),
|
||||
"opened now: /dir".to_string()
|
||||
],
|
||||
"Second opened buffer should fill the context, labels should be trimmed if long enough"
|
||||
);
|
||||
tasks_picker.update(cx, |_, cx| {
|
||||
cx.emit(DismissEvent);
|
||||
});
|
||||
drop(tasks_picker);
|
||||
cx.executor().run_until_parked();
|
||||
|
||||
let second_item = workspace
|
||||
.update(cx, |workspace, cx| {
|
||||
workspace.open_abs_path(PathBuf::from("/dir/file_without_extension"), true, cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let editor = cx.update(|cx| second_item.act_as::<Editor>(cx)).unwrap();
|
||||
editor.update(cx, |editor, cx| {
|
||||
editor.change_selections(None, cx, |s| {
|
||||
s.select_ranges(Some(Point::new(1, 2)..Point::new(1, 5)))
|
||||
})
|
||||
});
|
||||
cx.executor().run_until_parked();
|
||||
let tasks_picker = open_spawn_tasks(&workspace, cx);
|
||||
assert_eq!(
|
||||
task_names(&tasks_picker, cx),
|
||||
vec![
|
||||
"hello from …ithout_extension:2:3".to_string(),
|
||||
"opened now: /dir".to_string()
|
||||
],
|
||||
"Opened buffer should fill the context, labels should be trimmed if long enough"
|
||||
);
|
||||
tasks_picker.update(cx, |_, cx| {
|
||||
cx.emit(DismissEvent);
|
||||
});
|
||||
drop(tasks_picker);
|
||||
cx.executor().run_until_parked();
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_language_task_filtering(cx: &mut TestAppContext) {
|
||||
init_test(cx);
|
||||
let fs = FakeFs::new(cx.executor());
|
||||
fs.insert_tree(
|
||||
"/dir",
|
||||
json!({
|
||||
"a1.ts": "// a1",
|
||||
"a2.ts": "// a2",
|
||||
"b.rs": "// b",
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
|
||||
let project = Project::test(fs, ["/dir".as_ref()], cx).await;
|
||||
project.read_with(cx, |project, _| {
|
||||
let language_registry = project.languages();
|
||||
language_registry.add(Arc::new(
|
||||
Language::new(
|
||||
LanguageConfig {
|
||||
name: "TypeScript".into(),
|
||||
matcher: LanguageMatcher {
|
||||
path_suffixes: vec!["ts".to_string()],
|
||||
..LanguageMatcher::default()
|
||||
},
|
||||
..LanguageConfig::default()
|
||||
},
|
||||
None,
|
||||
)
|
||||
.with_context_provider(Some(Arc::new(
|
||||
ContextProviderWithTasks::new(TaskTemplates(vec![
|
||||
TaskTemplate {
|
||||
label: "Task without variables".to_string(),
|
||||
command: "npm run clean".to_string(),
|
||||
..TaskTemplate::default()
|
||||
},
|
||||
TaskTemplate {
|
||||
label: "TypeScript task from file $ZED_FILE".to_string(),
|
||||
command: "npm run build".to_string(),
|
||||
..TaskTemplate::default()
|
||||
},
|
||||
TaskTemplate {
|
||||
label: "Another task from file $ZED_FILE".to_string(),
|
||||
command: "npm run lint".to_string(),
|
||||
..TaskTemplate::default()
|
||||
},
|
||||
])),
|
||||
))),
|
||||
));
|
||||
language_registry.add(Arc::new(
|
||||
Language::new(
|
||||
LanguageConfig {
|
||||
name: "Rust".into(),
|
||||
matcher: LanguageMatcher {
|
||||
path_suffixes: vec!["rs".to_string()],
|
||||
..LanguageMatcher::default()
|
||||
},
|
||||
..LanguageConfig::default()
|
||||
},
|
||||
None,
|
||||
)
|
||||
.with_context_provider(Some(Arc::new(
|
||||
ContextProviderWithTasks::new(TaskTemplates(vec![TaskTemplate {
|
||||
label: "Rust task".to_string(),
|
||||
command: "cargo check".into(),
|
||||
..TaskTemplate::default()
|
||||
}])),
|
||||
))),
|
||||
));
|
||||
});
|
||||
let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
|
||||
|
||||
let _ts_file_1 = workspace
|
||||
.update(cx, |workspace, cx| {
|
||||
workspace.open_abs_path(PathBuf::from("/dir/a1.ts"), true, cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
let tasks_picker = open_spawn_tasks(&workspace, cx);
|
||||
assert_eq!(
|
||||
task_names(&tasks_picker, cx),
|
||||
vec![
|
||||
"Another task from file /dir/a1.ts",
|
||||
"TypeScript task from file /dir/a1.ts",
|
||||
"Task without variables",
|
||||
],
|
||||
"Should open spawn TypeScript tasks for the opened file, tasks with most template variables above, all groups sorted alphanumerically"
|
||||
);
|
||||
emulate_task_schedule(
|
||||
tasks_picker,
|
||||
&project,
|
||||
"TypeScript task from file /dir/a1.ts",
|
||||
cx,
|
||||
);
|
||||
|
||||
let tasks_picker = open_spawn_tasks(&workspace, cx);
|
||||
assert_eq!(
|
||||
task_names(&tasks_picker, cx),
|
||||
vec!["TypeScript task from file /dir/a1.ts", "Another task from file /dir/a1.ts", "Task without variables"],
|
||||
"After spawning the task and getting it into the history, it should be up in the sort as recently used"
|
||||
);
|
||||
tasks_picker.update(cx, |_, cx| {
|
||||
cx.emit(DismissEvent);
|
||||
});
|
||||
drop(tasks_picker);
|
||||
cx.executor().run_until_parked();
|
||||
|
||||
let _ts_file_2 = workspace
|
||||
.update(cx, |workspace, cx| {
|
||||
workspace.open_abs_path(PathBuf::from("/dir/a2.ts"), true, cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
let tasks_picker = open_spawn_tasks(&workspace, cx);
|
||||
assert_eq!(
|
||||
task_names(&tasks_picker, cx),
|
||||
vec![
|
||||
"TypeScript task from file /dir/a1.ts",
|
||||
"Another task from file /dir/a2.ts",
|
||||
"TypeScript task from file /dir/a2.ts",
|
||||
"Task without variables"
|
||||
],
|
||||
"Even when both TS files are open, should only show the history (on the top), and tasks, resolved for the current file"
|
||||
);
|
||||
tasks_picker.update(cx, |_, cx| {
|
||||
cx.emit(DismissEvent);
|
||||
});
|
||||
drop(tasks_picker);
|
||||
cx.executor().run_until_parked();
|
||||
|
||||
let _rs_file = workspace
|
||||
.update(cx, |workspace, cx| {
|
||||
workspace.open_abs_path(PathBuf::from("/dir/b.rs"), true, cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
let tasks_picker = open_spawn_tasks(&workspace, cx);
|
||||
assert_eq!(
|
||||
task_names(&tasks_picker, cx),
|
||||
vec!["Rust task"],
|
||||
"Even when both TS files are open and one TS task spawned, opened file's language tasks should be displayed only"
|
||||
);
|
||||
|
||||
cx.dispatch_action(CloseInactiveTabsAndPanes::default());
|
||||
emulate_task_schedule(tasks_picker, &project, "Rust task", cx);
|
||||
let _ts_file_2 = workspace
|
||||
.update(cx, |workspace, cx| {
|
||||
workspace.open_abs_path(PathBuf::from("/dir/a2.ts"), true, cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
let tasks_picker = open_spawn_tasks(&workspace, cx);
|
||||
assert_eq!(
|
||||
task_names(&tasks_picker, cx),
|
||||
vec![
|
||||
"TypeScript task from file /dir/a1.ts",
|
||||
"Another task from file /dir/a2.ts",
|
||||
"TypeScript task from file /dir/a2.ts",
|
||||
"Task without variables"
|
||||
],
|
||||
"After closing all but *.rs tabs, running a Rust task and switching back to TS tasks, \
|
||||
same TS spawn history should be restored"
|
||||
);
|
||||
}
|
||||
|
||||
fn emulate_task_schedule(
|
||||
tasks_picker: View<Picker<TasksModalDelegate>>,
|
||||
project: &Model<Project>,
|
||||
scheduled_task_label: &str,
|
||||
cx: &mut VisualTestContext,
|
||||
) {
|
||||
let scheduled_task = tasks_picker.update(cx, |tasks_picker, _| {
|
||||
tasks_picker
|
||||
.delegate
|
||||
.candidates
|
||||
.iter()
|
||||
.flatten()
|
||||
.find(|(_, task)| task.resolved_label == scheduled_task_label)
|
||||
.cloned()
|
||||
.unwrap()
|
||||
});
|
||||
project.update(cx, |project, cx| {
|
||||
project.task_inventory().update(cx, |inventory, _| {
|
||||
let (kind, task) = scheduled_task;
|
||||
inventory.task_scheduled(kind, task);
|
||||
})
|
||||
});
|
||||
tasks_picker.update(cx, |_, cx| {
|
||||
cx.emit(DismissEvent);
|
||||
});
|
||||
drop(tasks_picker);
|
||||
cx.executor().run_until_parked()
|
||||
}
|
||||
|
||||
fn open_spawn_tasks(
|
||||
workspace: &View<Workspace>,
|
||||
cx: &mut VisualTestContext,
|
||||
@@ -569,7 +881,7 @@ mod tests {
|
||||
workspace.update(cx, |workspace, cx| {
|
||||
workspace
|
||||
.active_modal::<TasksModal>(cx)
|
||||
.unwrap()
|
||||
.expect("no task modal after `Spawn` action was dispatched")
|
||||
.read(cx)
|
||||
.picker
|
||||
.clone()
|
||||
|
||||
@@ -39,7 +39,7 @@ use pty_info::PtyProcessInfo;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use settings::Settings;
|
||||
use smol::channel::{Receiver, Sender};
|
||||
use task::{RevealStrategy, TaskId};
|
||||
use task::TaskId;
|
||||
use terminal_settings::{AlternateScroll, Shell, TerminalBlink, TerminalSettings};
|
||||
use theme::{ActiveTheme, Theme};
|
||||
use util::truncate_and_trailoff;
|
||||
@@ -286,17 +286,6 @@ impl Display for TerminalError {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct SpawnTask {
|
||||
pub id: TaskId,
|
||||
pub full_label: String,
|
||||
pub label: String,
|
||||
pub command: String,
|
||||
pub args: Vec<String>,
|
||||
pub env: HashMap<String, String>,
|
||||
pub reveal: RevealStrategy,
|
||||
}
|
||||
|
||||
// https://github.com/alacritty/alacritty/blob/cb3a79dbf6472740daca8440d5166c1d4af5029e/extra/man/alacritty.5.scd?plain=1#L207-L213
|
||||
const DEFAULT_SCROLL_HISTORY_LINES: usize = 10_000;
|
||||
const MAX_SCROLL_HISTORY_LINES: usize = 100_000;
|
||||
|
||||
@@ -28,7 +28,6 @@ search.workspace = true
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
settings.workspace = true
|
||||
shlex.workspace = true
|
||||
shellexpand.workspace = true
|
||||
smol.workspace = true
|
||||
terminal.workspace = true
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use std::{borrow::Cow, ops::ControlFlow, path::PathBuf, sync::Arc};
|
||||
use std::{ops::ControlFlow, path::PathBuf, sync::Arc};
|
||||
|
||||
use crate::TerminalView;
|
||||
use collections::{HashMap, HashSet};
|
||||
@@ -15,10 +15,7 @@ use search::{buffer_search::DivRegistrar, BufferSearchBar};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use settings::Settings;
|
||||
use task::{RevealStrategy, SpawnInTerminal, TaskId};
|
||||
use terminal::{
|
||||
terminal_settings::{Shell, TerminalDockPosition, TerminalSettings},
|
||||
SpawnTask,
|
||||
};
|
||||
use terminal::terminal_settings::{Shell, TerminalDockPosition, TerminalSettings};
|
||||
use ui::{h_flex, ButtonCommon, Clickable, IconButton, IconSize, Selectable, Tooltip};
|
||||
use util::{ResultExt, TryFutureExt};
|
||||
use workspace::{
|
||||
@@ -302,36 +299,31 @@ impl TerminalPanel {
|
||||
}
|
||||
|
||||
fn spawn_task(&mut self, spawn_in_terminal: &SpawnInTerminal, cx: &mut ViewContext<Self>) {
|
||||
let mut spawn_task = SpawnTask {
|
||||
id: spawn_in_terminal.id.clone(),
|
||||
full_label: spawn_in_terminal.full_label.clone(),
|
||||
label: spawn_in_terminal.label.clone(),
|
||||
command: spawn_in_terminal.command.clone(),
|
||||
args: spawn_in_terminal.args.clone(),
|
||||
env: spawn_in_terminal.env.clone(),
|
||||
reveal: spawn_in_terminal.reveal,
|
||||
};
|
||||
let mut spawn_task = spawn_in_terminal.clone();
|
||||
// Set up shell args unconditionally, as tasks are always spawned inside of a shell.
|
||||
let Some((shell, mut user_args)) = (match TerminalSettings::get_global(cx).shell.clone() {
|
||||
Shell::System => std::env::var("SHELL").ok().map(|shell| (shell, vec![])),
|
||||
Shell::Program(shell) => Some((shell, vec![])),
|
||||
Shell::System => std::env::var("SHELL").ok().map(|shell| (shell, Vec::new())),
|
||||
Shell::Program(shell) => Some((shell, Vec::new())),
|
||||
Shell::WithArguments { program, args } => Some((program, args)),
|
||||
}) else {
|
||||
return;
|
||||
};
|
||||
|
||||
let mut command = std::mem::take(&mut spawn_task.command);
|
||||
let args = std::mem::take(&mut spawn_task.args);
|
||||
for arg in args {
|
||||
command.push(' ');
|
||||
let arg = shlex::try_quote(&arg).unwrap_or(Cow::Borrowed(&arg));
|
||||
command.push_str(&arg);
|
||||
}
|
||||
spawn_task.command = shell;
|
||||
user_args.extend(["-i".to_owned(), "-c".to_owned(), command]);
|
||||
spawn_task.command_label = format!("{shell} -i -c `{}`", spawn_task.command_label);
|
||||
let task_command = std::mem::replace(&mut spawn_task.command, shell);
|
||||
let task_args = std::mem::take(&mut spawn_task.args);
|
||||
let combined_command = task_args
|
||||
.into_iter()
|
||||
.fold(task_command, |mut command, arg| {
|
||||
command.push(' ');
|
||||
command.push_str(&arg);
|
||||
command
|
||||
});
|
||||
user_args.extend(["-i".to_owned(), "-c".to_owned(), combined_command]);
|
||||
spawn_task.args = user_args;
|
||||
let reveal = spawn_task.reveal;
|
||||
let spawn_task = spawn_task;
|
||||
|
||||
let reveal = spawn_task.reveal;
|
||||
let working_directory = spawn_in_terminal.cwd.clone();
|
||||
let allow_concurrent_runs = spawn_in_terminal.allow_concurrent_runs;
|
||||
let use_new_terminal = spawn_in_terminal.use_new_terminal;
|
||||
@@ -407,7 +399,7 @@ impl TerminalPanel {
|
||||
|
||||
fn spawn_in_new_terminal(
|
||||
&mut self,
|
||||
spawn_task: SpawnTask,
|
||||
spawn_task: SpawnInTerminal,
|
||||
working_directory: Option<PathBuf>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) {
|
||||
@@ -470,7 +462,7 @@ impl TerminalPanel {
|
||||
fn add_terminal(
|
||||
&mut self,
|
||||
working_directory: Option<PathBuf>,
|
||||
spawn_task: Option<SpawnTask>,
|
||||
spawn_task: Option<SpawnInTerminal>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) {
|
||||
let workspace = self.workspace.clone();
|
||||
@@ -562,7 +554,7 @@ impl TerminalPanel {
|
||||
fn replace_terminal(
|
||||
&self,
|
||||
working_directory: Option<PathBuf>,
|
||||
spawn_task: SpawnTask,
|
||||
spawn_task: SpawnInTerminal,
|
||||
terminal_item_index: usize,
|
||||
terminal_to_replace: View<TerminalView>,
|
||||
cx: &mut ViewContext<'_, Self>,
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
description = "The fast, collaborative code editor."
|
||||
edition = "2021"
|
||||
name = "zed"
|
||||
version = "0.132.0"
|
||||
version = "0.132.2"
|
||||
publish = false
|
||||
license = "GPL-3.0-or-later"
|
||||
authors = ["Zed Team <hi@zed.dev>"]
|
||||
|
||||
@@ -1 +1 @@
|
||||
dev
|
||||
stable
|
||||
@@ -17,25 +17,31 @@ You will need write access to the Zed repository to do this:
|
||||
- Run `./script/bump-zed-minor-versions` and push the tags
|
||||
and branches as instructed.
|
||||
- Wait for the builds to appear at https://github.com/zed-industries/zed/releases (typically takes around 30 minutes)
|
||||
- Copy the release notes from the previous Preview release(s) to the current Stable release.
|
||||
- Write new release notes for Preview. `/script/get-preview-channel-changes` can help with this, but you'll need to edit and format the output to make it good.
|
||||
- Download the artifacts for each release and test that you can run them locally.
|
||||
- Publish the releases.
|
||||
- While you're waiting:
|
||||
- Start creating the new release notes for preview. You can start with the output of `./script/get-preview-channel-changes`.
|
||||
- Start drafting the release tweets.
|
||||
- Once the builds are ready:
|
||||
- Copy the release notes from the previous Preview release(s) to the current Stable release.
|
||||
- Download the artifacts for each release and test that you can run them locally.
|
||||
- Publish the releases on GitHub.
|
||||
- Tweet the tweets (Credentials are in 1password).
|
||||
|
||||
## Patch release process
|
||||
|
||||
If your PR fixes a panic or a crash, you should cherry-pick it to the current stable and preview branches. If your PR fixes a regression in recently released code, you should cherry-pick it to the appropriate branch.
|
||||
If your PR fixes a panic or a crash, you should cherry-pick it to the current stable and preview branches. If your PR fixes a regression in recently released code, you should cherry-pick it to preview.
|
||||
|
||||
You will need write access to the Zed repository to do this:
|
||||
|
||||
- Cherry pick them onto the correct branch. You can either do this manually, or leave a comment of the form `/cherry-pick v0.XXX.x` on the PR, and the GitHub bot should do it for you.
|
||||
- Run `./script/trigger-release {preview|stable}`
|
||||
- Send a PR containing your change to `main` as normal.
|
||||
- Leave a comment on the PR `/cherry-pick v0.XXX.x`. Once your PR is merged, the Github bot will send a PR to the branch.
|
||||
- In case of a merge conflict, you will have to cherry-pick manually and push the change to the `v0.XXX.x` branch.
|
||||
- After the commits are cherry-picked onto the branch, run `./script/trigger-release {preview|stable}`. This will bump the version numbers, create a new release tag, and kick off a release build.
|
||||
- Wait for the builds to appear at https://github.com/zed-industries/zed/releases (typically takes around 30 minutes)
|
||||
- Add release notes using the `Release notes:` section of each cherry-picked PR.
|
||||
- Proof-read and edit the release notes as needed.
|
||||
- Download the artifacts for each release and test that you can run them locally.
|
||||
- Publish the release.
|
||||
|
||||
## Nightly release process
|
||||
|
||||
- Merge your changes to main
|
||||
- Run `./script/trigger-release {nightly}`
|
||||
In addition to the public releases, we also have a nightly build that we encourage employees to use.
|
||||
Nightly is released by cron once a day, and can be shipped as often as you'd like. There are no release notes or announcements, so you can just merge your changes to main and run `./script/trigger-release nightly`.
|
||||
|
||||
109
script/draft-release-notes
Executable file
109
script/draft-release-notes
Executable file
@@ -0,0 +1,109 @@
|
||||
#!/usr/bin/env node --redirect-warnings=/dev/null
|
||||
|
||||
const { execFileSync } = require("child_process");
|
||||
|
||||
main();
|
||||
|
||||
async function main() {
|
||||
let version = process.argv[2];
|
||||
let channel = process.argv[3];
|
||||
let parts = version.split(".");
|
||||
|
||||
if (
|
||||
process.argv.length != 4 ||
|
||||
parts.length != 3 ||
|
||||
parts.find((part) => isNaN(part)) != null ||
|
||||
(channel != "stable" && channel != "preview")
|
||||
) {
|
||||
console.log("Usage: draft-release-notes <version> {stable|preview}");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
let priorVersion = [parts[0], parts[1], parts[2] - 1].join(".");
|
||||
let suffix = "";
|
||||
|
||||
if (channel == "preview") {
|
||||
suffix = "-pre";
|
||||
if (parts[2] == 0) {
|
||||
priorVersion = [parts[0], parts[1] - 1, 0].join(".");
|
||||
}
|
||||
} else if (!tagExists("v${priorVersion}")) {
|
||||
console.log("Copy the release notes from preview.");
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
let [tag, priorTag] = [`v${version}${suffix}`, `v${priorVersion}${suffix}`];
|
||||
|
||||
const newCommits = getCommits(priorTag, tag);
|
||||
|
||||
let releaseNotes = [];
|
||||
let missing = [];
|
||||
let skipped = [];
|
||||
|
||||
for (const commit of newCommits) {
|
||||
let link = "https://github.com/zed-industries/zed/pull/" + commit.pr;
|
||||
let notes = commit.releaseNotes;
|
||||
if (commit.pr == "") {
|
||||
link = "https://github.com/zed-industries/zed/commits/" + commit.hash;
|
||||
} else if (!notes.includes("zed-industries/zed/issues")) {
|
||||
notes = notes + " ([#" + commit.pr + "](" + link + "))";
|
||||
}
|
||||
|
||||
if (commit.releaseNotes == "") {
|
||||
missing.push("- MISSING " + commit.firstLine + " " + link);
|
||||
} else if (commit.releaseNotes.startsWith("- N/A")) {
|
||||
skipped.push("- N/A " + commit.firstLine + " " + link);
|
||||
} else {
|
||||
releaseNotes.push(notes);
|
||||
}
|
||||
}
|
||||
|
||||
console.log(releaseNotes.join("\n") + "\n");
|
||||
console.log("<!-- ");
|
||||
console.log(missing.join("\n"));
|
||||
console.log(skipped.join("\n"));
|
||||
console.log("-->");
|
||||
}
|
||||
|
||||
function getCommits(oldTag, newTag) {
|
||||
const pullRequestNumbers = execFileSync(
|
||||
"git",
|
||||
["log", `${oldTag}..${newTag}`, "--format=DIVIDER\n%H|||%B"],
|
||||
{ encoding: "utf8" },
|
||||
)
|
||||
.replace(/\r\n/g, "\n")
|
||||
.split("DIVIDER\n")
|
||||
.filter((commit) => commit.length > 0)
|
||||
.map((commit) => {
|
||||
let [hash, firstLine] = commit.split("\n")[0].split("|||");
|
||||
let cherryPick = firstLine.match(/\(cherry-pick #([0-9]+)\)/)?.[1] || "";
|
||||
let pr = firstLine.match(/\(#(\d+)\)$/)?.[1] || "";
|
||||
let releaseNotes = (commit.split(/Release notes:.*\n/i)[1] || "")
|
||||
.split("\n\n")[0]
|
||||
.trim()
|
||||
.replace(/\n(?![\n-])/g, " ");
|
||||
|
||||
if (releaseNotes.includes("<public_issue_number_if_exists>")) {
|
||||
releaseNotes = "";
|
||||
}
|
||||
|
||||
return {
|
||||
hash,
|
||||
pr,
|
||||
cherryPick,
|
||||
releaseNotes,
|
||||
firstLine,
|
||||
};
|
||||
});
|
||||
|
||||
return pullRequestNumbers;
|
||||
}
|
||||
|
||||
function tagExists(tag) {
|
||||
try {
|
||||
execFileSync("git", ["rev-parse", "--verify", tag]);
|
||||
return true;
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user