Sprint B.5: add-LICENSE action (MIT/Apache-2.0) in [m] menu + vulp repo license
- src/license/: embedded templates, holder resolution (config > git user.name > account username), write path shared by TUI and CLI - TUI: bulk-aware [m] entry over action_targets(), kind select + confirm, point-updates has_license/is_dirty, logs to activity (new License kind) - CLI: vulp repo license <repo>...|--all [--kind] [--holder] [--json], skips already-licensed repos, exit 2/4 contract kept - config: license_holder + default_license fields (TUI-editable) - never overwrites an existing license, never auto-commits
This commit is contained in:
1 parent
0468c032c6
commit
f1432d1cd3
15 files changed
+506
-7
No files matched your search
+10
-2
@@ -13,7 +13,7 @@ vulp <command> [--json] # non-interactive (scripts/AI)
|
||||
|
||||
- **`--json`** (global flag): machine-readable JSON on stdout. Available on:
|
||||
`list`, `status`, `scan`, `stats`, `activity`, `todo list`, `pr list`,
|
||||
`issue list`, `project list`, `accounts`, `refresh`.
|
||||
`issue list`, `project list`, `accounts`, `refresh`, `repo license`.
|
||||
- **No prompts, ever.** Missing input = error + non-zero exit.
|
||||
- **stdout** = results only. **stderr** = progress and errors.
|
||||
- **Exit codes:** `0` ok · `1` execution error · `2` bad usage ·
|
||||
@@ -30,7 +30,7 @@ vulp scan # repo count + dirty/behind/ahead summa
|
||||
vulp list [--dirty] [--behind] [--ahead] [--lang ts] [--project NAME]
|
||||
vulp status <repo> # branch, dirty, ahead/behind, last commit...
|
||||
vulp stats [--period 1d|7d|30d|90d|1y|all] # commits per repo in period
|
||||
vulp activity [--limit N] [--kind push|pull|release|"new repo"|todo|pr|issue|commit]
|
||||
vulp activity [--limit N] [--kind push|pull|release|"new repo"|todo|pr|issue|commit|license]
|
||||
```
|
||||
|
||||
`vulp list --json` returns an array of repo objects:
|
||||
@@ -96,8 +96,16 @@ vulp repo label <repo> "Display Name" # empty name clears the label
|
||||
vulp repo bookmark <repo> # toggle
|
||||
vulp repo pin <repo> # toggle
|
||||
vulp repo collab <repo> <user> [--role pull|push|admin]
|
||||
vulp repo license <repo>... [--kind mit|apache-2.0] [--holder NAME]
|
||||
vulp repo license --all # every repo missing a license
|
||||
```
|
||||
|
||||
`repo license` writes a `LICENSE` file into each target repo. It never
|
||||
overwrites an existing license (reported as skipped) and never commits —
|
||||
review and commit the file yourself. Defaults come from config:
|
||||
`default_license` (mit) and `license_holder` (falls back to
|
||||
`git config user.name`, then the account username).
|
||||
|
||||
## Recipes for AI agents
|
||||
|
||||
```bash
|
||||
|
||||
+3
-2
@@ -8,7 +8,7 @@ use std::path::PathBuf;
|
||||
|
||||
/// The kind of action recorded in one activity log entry.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub enum ActivityKind { Commit, Push, Pull, Release, NewRepo, Todo, Pr, Issue }
|
||||
pub enum ActivityKind { Commit, Push, Pull, Release, NewRepo, Todo, Pr, Issue, License }
|
||||
|
||||
impl ActivityKind {
|
||||
/// Display label shown in the Activity tab's table.
|
||||
@@ -22,6 +22,7 @@ impl ActivityKind {
|
||||
Self::Todo => "todo",
|
||||
Self::Pr => "pr",
|
||||
Self::Issue => "issue",
|
||||
Self::License => "license",
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,7 +31,7 @@ impl ActivityKind {
|
||||
pub fn cycle() -> &'static [ActivityKind] {
|
||||
&[
|
||||
Self::Commit, Self::Push, Self::Pull, Self::Release,
|
||||
Self::NewRepo, Self::Todo, Self::Pr, Self::Issue,
|
||||
Self::NewRepo, Self::Todo, Self::Pr, Self::Issue, Self::License,
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
+11
@@ -208,6 +208,17 @@ pub enum RepoCmd {
|
||||
/// pull | push | admin
|
||||
#[arg(long, default_value = "push")] role: String,
|
||||
},
|
||||
/// Write a LICENSE file into repos that lack one (never overwrites, never commits)
|
||||
License {
|
||||
/// Repo names (omit with --all)
|
||||
repos: Vec<String>,
|
||||
/// Every workspace repo missing a license
|
||||
#[arg(long)] all: bool,
|
||||
/// mit | apache-2.0 (default: config `default_license`)
|
||||
#[arg(long)] kind: Option<String>,
|
||||
/// Copyright holder (default: config `license_holder`, then git user.name)
|
||||
#[arg(long)] holder: Option<String>,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Args)]
|
||||
|
||||
+1
-1
@@ -34,7 +34,7 @@ pub async fn run(cmd: Commands, json: bool, cfg: &AppConfig) -> Result<()> {
|
||||
|
||||
Commands::Pr { action } => remote::pr(cfg, json, action).await,
|
||||
Commands::Issue { action } => remote::issue(cfg, json, action).await,
|
||||
Commands::Repo { action } => remote::repo_cmd(cfg, action).await,
|
||||
Commands::Repo { action } => remote::repo_cmd(cfg, json, action).await,
|
||||
Commands::Init { name, private, description } =>
|
||||
remote::init(cfg, &name, private, &description).await,
|
||||
Commands::Accounts => remote::accounts(cfg, json),
|
||||
|
||||
+68
-1
@@ -103,7 +103,7 @@ pub async fn release(cfg: &AppConfig, repo: &str, tag: &str, notes: &str) -> Res
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn repo_cmd(cfg: &AppConfig, cmd: RepoCmd) -> Result<()> {
|
||||
pub async fn repo_cmd(cfg: &AppConfig, json: bool, cmd: RepoCmd) -> Result<()> {
|
||||
match cmd {
|
||||
RepoCmd::Url { repo } => {
|
||||
let r = find_repo(cfg, &repo);
|
||||
@@ -170,7 +170,74 @@ pub async fn repo_cmd(cfg: &AppConfig, cmd: RepoCmd) -> Result<()> {
|
||||
Err(e) => die(1, &format!("invite failed: {e:#}")),
|
||||
}
|
||||
}
|
||||
RepoCmd::License { repos, all, kind, holder } => {
|
||||
license(cfg, json, repos, all, kind, holder)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// `vulp repo license` — writes a LICENSE into each target repo that lacks
|
||||
/// one. Explicit names that already have a license are reported as skipped,
|
||||
/// not errors; `--all` targets every unlicensed workspace repo.
|
||||
fn license(cfg: &AppConfig, json: bool, repos: Vec<String>, all: bool,
|
||||
kind: Option<String>, holder: Option<String>) -> Result<()> {
|
||||
use crate::license::{LicenseKind, WriteOutcome};
|
||||
|
||||
if repos.is_empty() && !all {
|
||||
die(2, "pass repo names or --all");
|
||||
}
|
||||
let kind = match kind {
|
||||
Some(s) => match LicenseKind::parse(&s) {
|
||||
Some(k) => k,
|
||||
None => die(2, "kind must be mit | apache-2.0"),
|
||||
},
|
||||
None => crate::license::default_kind(cfg),
|
||||
};
|
||||
let holder = holder.unwrap_or_else(|| crate::license::holder_for(cfg));
|
||||
if holder.trim().is_empty() {
|
||||
die(2, "no copyright holder — pass --holder or set license_holder/username in config");
|
||||
}
|
||||
|
||||
let workspace = super::scan_repos(cfg);
|
||||
let targets: Vec<&crate::status::RepoSummary> = if all {
|
||||
workspace.iter().filter(|r| !r.has_license).collect()
|
||||
} else {
|
||||
repos.iter().map(|name| {
|
||||
let needle = name.to_lowercase();
|
||||
workspace.iter().find(|r| {
|
||||
r.name.to_lowercase() == needle
|
||||
|| r.path.file_name().map(|b| b.to_string_lossy().to_lowercase() == needle).unwrap_or(false)
|
||||
}).unwrap_or_else(|| die(4, &format!("repo '{name}' not found in {}", cfg.workspace_root.display())))
|
||||
}).collect()
|
||||
};
|
||||
|
||||
let year = chrono::Datelike::year(&chrono::Utc::now());
|
||||
let (mut written, mut skipped, mut failed) = (Vec::new(), Vec::new(), Vec::new());
|
||||
for r in targets {
|
||||
match crate::license::write_license(&r.path, kind, &holder, year) {
|
||||
Ok(WriteOutcome::Written) => {
|
||||
log_activity(ActivityKind::License, &r.name, format!("{} ({})", kind.label(), holder));
|
||||
written.push(r.name.clone());
|
||||
}
|
||||
Ok(WriteOutcome::AlreadyLicensed) => skipped.push(r.name.clone()),
|
||||
Err(e) => {
|
||||
eprintln!("error: writing LICENSE to {} failed: {e:#}", r.name);
|
||||
failed.push(r.name.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if json {
|
||||
#[derive(serde::Serialize)]
|
||||
struct Out { kind: String, holder: String, written: Vec<String>, skipped: Vec<String>, failed: Vec<String> }
|
||||
out_json(&Out { kind: kind.label().into(), holder, written, skipped, failed: failed.clone() });
|
||||
} else {
|
||||
for n in &written { println!("LICENSE ({}) written to {n}", kind.label()); }
|
||||
for n in &skipped { println!("skipped {n} (already has a license)"); }
|
||||
if written.is_empty() && skipped.is_empty() { println!("nothing to do"); }
|
||||
}
|
||||
if !failed.is_empty() { std::process::exit(1); }
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn init(cfg: &AppConfig, name: &str, private: bool, description: &str) -> Result<()> {
|
||||
|
||||
@@ -37,8 +37,17 @@ pub struct AppConfig {
|
||||
/// Count lines-of-code during workspace scan (slow for large repos; off by default).
|
||||
#[serde(default)]
|
||||
pub show_locs: bool,
|
||||
/// Copyright holder written into generated LICENSE files
|
||||
/// (empty = fall back to `git config user.name`, then `username`).
|
||||
#[serde(default)]
|
||||
pub license_holder: String,
|
||||
/// Default template for the add-LICENSE action: "mit" or "apache-2.0".
|
||||
#[serde(default = "default_license_kind")]
|
||||
pub default_license: String,
|
||||
}
|
||||
|
||||
fn default_license_kind() -> String { "mit".into() }
|
||||
|
||||
/// Picks the first available clipboard tool on PATH as the default
|
||||
/// `clipboard_cmd`, so a fresh install works without manual configuration
|
||||
/// on most systems.
|
||||
@@ -71,6 +80,8 @@ impl Default for AppConfig {
|
||||
workspace_groups: vec![],
|
||||
clipboard_cmd: default_clipboard_cmd(),
|
||||
show_locs: false,
|
||||
license_holder: String::new(),
|
||||
default_license: default_license_kind(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -94,6 +105,8 @@ impl AppConfig {
|
||||
("timezone", self.timezone.clone()),
|
||||
("vsc_data_dir", self.vsc_data_dir.to_string_lossy().into()),
|
||||
("workspace_groups", self.workspace_groups.join(", ")),
|
||||
("license_holder", self.license_holder.clone()),
|
||||
("default_license", self.default_license.clone()),
|
||||
]
|
||||
}
|
||||
|
||||
@@ -115,6 +128,8 @@ impl AppConfig {
|
||||
"share_stats" => self.share_stats = value == "true",
|
||||
"timezone" => self.timezone = value,
|
||||
"vsc_data_dir" => self.vsc_data_dir = PathBuf::from(&value),
|
||||
"license_holder" => self.license_holder = value,
|
||||
"default_license" => self.default_license = value,
|
||||
"workspace_groups" => {
|
||||
self.workspace_groups = value
|
||||
.split(',')
|
||||
|
||||
@@ -6,7 +6,7 @@ use super::modals_spawn::{
|
||||
spawn_convention_preview, spawn_create_issue, spawn_create_pr, spawn_find_usages,
|
||||
spawn_list_functions, spawn_repo_stats, spawn_rename_preview,
|
||||
};
|
||||
use super::state::{HubState, MenuAction, Modal, PhaseResult, SelectTag, SortMode, TextTag};
|
||||
use super::state::{ConfirmTag, HubState, MenuAction, Modal, PhaseResult, SelectTag, SortMode, TextTag};
|
||||
|
||||
/// Handles one step of a sort/filter, repo-creation, sync, Quick Actions, or
|
||||
/// code-intel modal chain. Returns `None` once `modal` has been consumed by
|
||||
@@ -157,6 +157,25 @@ pub fn try_confirm(state: &mut HubState, modal: Modal) -> Option<Modal> {
|
||||
}
|
||||
}
|
||||
Some(MenuAction::CodeIntelPicker) => super::modals_spawn::open_codeintel_picker(state),
|
||||
Some(MenuAction::AddLicense) => {
|
||||
let targets: Vec<usize> = state.action_targets().into_iter()
|
||||
.filter(|&ri| !state.repos[ri].has_license)
|
||||
.collect();
|
||||
if targets.is_empty() {
|
||||
state.status_msg = Some("All targeted repos already have a LICENSE".into());
|
||||
} else {
|
||||
let cursor = match crate::license::default_kind(&state.cfg) {
|
||||
crate::license::LicenseKind::Apache2 => 1,
|
||||
crate::license::LicenseKind::Mit => 0,
|
||||
};
|
||||
state.modal = Modal::Select {
|
||||
title: format!("Add LICENSE — {} repo{}", targets.len(), if targets.len() == 1 { "" } else { "s" }),
|
||||
options: vec!["MIT".into(), "Apache-2.0".into()],
|
||||
cursor,
|
||||
tag: SelectTag::LicenseKind { targets },
|
||||
};
|
||||
}
|
||||
}
|
||||
Some(MenuAction::StashSave(ri)) | Some(MenuAction::StashPop(ri)) => {
|
||||
let ri = *ri;
|
||||
let repo = state.repos.get(ri)?;
|
||||
@@ -187,6 +206,61 @@ pub fn try_confirm(state: &mut HubState, modal: Modal) -> Option<Modal> {
|
||||
None
|
||||
}
|
||||
|
||||
// ── Add LICENSE ([m] action, bulk over action_targets) ────────────────
|
||||
Modal::Select { cursor, tag: SelectTag::LicenseKind { targets }, .. } => {
|
||||
let kind = if cursor == 1 { crate::license::LicenseKind::Apache2 } else { crate::license::LicenseKind::Mit };
|
||||
let names: Vec<&str> = targets.iter().take(6)
|
||||
.filter_map(|&ri| state.repos.get(ri).map(|r| r.name.as_str()))
|
||||
.collect();
|
||||
let more = targets.len().saturating_sub(names.len());
|
||||
let list = if more > 0 {
|
||||
format!("{} +{} more", names.join(", "), more)
|
||||
} else {
|
||||
names.join(", ")
|
||||
};
|
||||
state.modal = Modal::Confirm {
|
||||
message: format!(
|
||||
"Write {} LICENSE to {} repo{}? ({}) — not committed",
|
||||
kind.label(), targets.len(), if targets.len() == 1 { "" } else { "s" }, list,
|
||||
),
|
||||
tag: ConfirmTag::WriteLicense { kind, targets },
|
||||
};
|
||||
None
|
||||
}
|
||||
Modal::Confirm { tag: ConfirmTag::WriteLicense { kind, targets }, .. } => {
|
||||
let holder = crate::license::holder_for(&state.cfg);
|
||||
let year = chrono::Datelike::year(&chrono::Utc::now());
|
||||
let (mut written, mut skipped, mut failed) = (0usize, 0usize, 0usize);
|
||||
for ri in targets {
|
||||
let Some(repo) = state.repos.get(ri) else { continue };
|
||||
let (path, name) = (repo.path.clone(), repo.name.clone());
|
||||
match crate::license::write_license(&path, kind, &holder, year) {
|
||||
Ok(crate::license::WriteOutcome::Written) => {
|
||||
written += 1;
|
||||
state.repos[ri].has_license = true;
|
||||
// New untracked LICENSE file — the repo is dirty now.
|
||||
state.repos[ri].is_dirty = true;
|
||||
crate::activity::log(&crate::activity::ActivityEntry {
|
||||
ts: chrono::Utc::now().timestamp(),
|
||||
kind: crate::activity::ActivityKind::License,
|
||||
repo: name,
|
||||
detail: format!("{} ({})", kind.label(), holder),
|
||||
});
|
||||
}
|
||||
Ok(crate::license::WriteOutcome::AlreadyLicensed) => {
|
||||
skipped += 1;
|
||||
state.repos[ri].has_license = true;
|
||||
}
|
||||
Err(_) => failed += 1,
|
||||
}
|
||||
}
|
||||
let mut msg = format!("LICENSE ({}): {} written", kind.label(), written);
|
||||
if skipped > 0 { msg.push_str(&format!(", {} already licensed", skipped)); }
|
||||
if failed > 0 { msg.push_str(&format!(", {} FAILED", failed)); }
|
||||
state.status_msg = Some(msg);
|
||||
None
|
||||
}
|
||||
|
||||
Modal::Text { value, tag: TextTag::PrRepo, .. } => {
|
||||
let repo = value.trim().to_string();
|
||||
if repo.is_empty() { return None; }
|
||||
|
||||
@@ -609,6 +609,12 @@ pub fn open_action_menu(state: &mut HubState) {
|
||||
entries.push(entry("Delete project", "Del", kc(KeyCode::Delete)));
|
||||
}
|
||||
|
||||
// Bulk-aware (multi-select > cursor repo > project): only shown when at
|
||||
// least one targeted repo is missing a license file.
|
||||
if state.action_targets().iter().any(|&ri| !state.repos[ri].has_license) {
|
||||
entries.push(entry("Add LICENSE (MIT/Apache)", "", MenuAction::AddLicense));
|
||||
}
|
||||
|
||||
match state.tab {
|
||||
Tab::Todos => {
|
||||
entries.push(entry("Cycle todo status", "Space", kc(KeyCode::Char(' '))));
|
||||
|
||||
@@ -81,6 +81,8 @@ pub enum MenuAction {
|
||||
/// `git stash` / `git stash pop` on the repo under the cursor.
|
||||
StashSave(usize),
|
||||
StashPop(usize),
|
||||
/// Write a LICENSE file into every `action_targets()` repo missing one.
|
||||
AddLicense,
|
||||
}
|
||||
|
||||
/// What a `Modal::Select` list is *for*, mirroring `TextTag`.
|
||||
@@ -121,6 +123,8 @@ pub enum SelectTag {
|
||||
ProfileScope(ProfileScopeAction),
|
||||
// Contextual `[m]` action menu — options paired 1:1 with the actions
|
||||
ActionMenu(Vec<MenuAction>),
|
||||
// add-LICENSE flow: MIT vs Apache-2.0, for the captured target repos
|
||||
LicenseKind { targets: Vec<usize> },
|
||||
// Code intelligence (Stats tab, [m])
|
||||
CodeIntelRepoPick,
|
||||
CodeIntelAction { repo: std::path::PathBuf, name: String },
|
||||
@@ -154,6 +158,8 @@ pub enum ConfirmTag {
|
||||
DeleteTodo(usize),
|
||||
RemoveGitAll,
|
||||
DeleteTemplate(usize),
|
||||
// write a LICENSE (of the chosen kind) into each target repo
|
||||
WriteLicense { kind: crate::license::LicenseKind, targets: Vec<usize> },
|
||||
}
|
||||
|
||||
/// Outcome of one key event while a modal is open, returned by
|
||||
|
||||
@@ -27,6 +27,7 @@ fn kind_color(kind: crate::activity::ActivityKind) -> Color {
|
||||
K::Todo => Color::Yellow,
|
||||
K::Pr => Color::Cyan,
|
||||
K::Issue => Color::Yellow,
|
||||
K::License => Color::Magenta,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -185,6 +185,7 @@ pub fn render_activity_global(f: &mut Frame, state: &HubState, area: Rect) {
|
||||
crate::activity::ActivityKind::Todo => Style::default().fg(Color::Yellow),
|
||||
crate::activity::ActivityKind::Pr => Style::default().fg(Color::Cyan),
|
||||
crate::activity::ActivityKind::Issue => Style::default().fg(Color::Yellow),
|
||||
crate::activity::ActivityKind::License => Style::default().fg(Color::Magenta),
|
||||
};
|
||||
let detail = e.detail.chars().take(detail_w).collect::<String>();
|
||||
Row::new(vec![
|
||||
|
||||
@@ -0,0 +1,201 @@
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright {year} {holder}
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) {year} {holder}
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -0,0 +1,86 @@
|
||||
//! LICENSE file generation: embedded MIT / Apache-2.0 templates, holder
|
||||
//! resolution (config override → git user.name → account username), and the
|
||||
//! shared write path used by both the hub's [m] action and `vulp repo
|
||||
//! license`. Writing never overwrites an existing license and never commits.
|
||||
|
||||
use crate::config::AppConfig;
|
||||
use anyhow::Result;
|
||||
use std::path::Path;
|
||||
|
||||
/// Which license template to write.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum LicenseKind { Mit, Apache2 }
|
||||
|
||||
impl LicenseKind {
|
||||
/// Display / SPDX-style label.
|
||||
pub fn label(&self) -> &'static str {
|
||||
match self {
|
||||
Self::Mit => "MIT",
|
||||
Self::Apache2 => "Apache-2.0",
|
||||
}
|
||||
}
|
||||
|
||||
/// Parses a user-supplied kind string ("mit", "apache-2.0", "apache2"...).
|
||||
pub fn parse(s: &str) -> Option<Self> {
|
||||
match s.trim().to_lowercase().as_str() {
|
||||
"mit" => Some(Self::Mit),
|
||||
"apache-2.0" | "apache2" | "apache-2" | "apache" => Some(Self::Apache2),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn template(&self) -> &'static str {
|
||||
match self {
|
||||
Self::Mit => include_str!("MIT.txt"),
|
||||
Self::Apache2 => include_str!("APACHE-2.0.txt"),
|
||||
}
|
||||
}
|
||||
|
||||
/// The full LICENSE file body with `{year}`/`{holder}` filled in.
|
||||
pub fn render(&self, holder: &str, year: i32) -> String {
|
||||
self.template()
|
||||
.replace("{year}", &year.to_string())
|
||||
.replace("{holder}", holder)
|
||||
}
|
||||
}
|
||||
|
||||
/// Outcome of one `write_license` call, so callers can report per-repo.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum WriteOutcome { Written, AlreadyLicensed }
|
||||
|
||||
/// Writes a `LICENSE` file at `repo`'s root. Skips (never overwrites) when
|
||||
/// the repo already has a recognizable license file. Does NOT commit —
|
||||
/// the user reviews and commits the file like any other change.
|
||||
pub fn write_license(repo: &Path, kind: LicenseKind, holder: &str, year: i32) -> Result<WriteOutcome> {
|
||||
if crate::status::detect_license(repo) {
|
||||
return Ok(WriteOutcome::AlreadyLicensed);
|
||||
}
|
||||
std::fs::write(repo.join("LICENSE"), kind.render(holder, year))?;
|
||||
Ok(WriteOutcome::Written)
|
||||
}
|
||||
|
||||
/// Resolves the copyright holder: `license_holder` from config when set,
|
||||
/// otherwise global `git config user.name`, otherwise the configured
|
||||
/// account username.
|
||||
pub fn holder_for(cfg: &AppConfig) -> String {
|
||||
let configured = cfg.license_holder.trim();
|
||||
if !configured.is_empty() {
|
||||
return configured.to_string();
|
||||
}
|
||||
if let Ok(out) = std::process::Command::new("git")
|
||||
.args(["config", "--get", "user.name"])
|
||||
.output()
|
||||
{
|
||||
let name = String::from_utf8_lossy(&out.stdout).trim().to_string();
|
||||
if !name.is_empty() {
|
||||
return name;
|
||||
}
|
||||
}
|
||||
cfg.username.clone()
|
||||
}
|
||||
|
||||
/// The configured default kind ("mit" unless overridden), falling back to
|
||||
/// MIT on an unparsable config value rather than erroring.
|
||||
pub fn default_kind(cfg: &AppConfig) -> LicenseKind {
|
||||
LicenseKind::parse(&cfg.default_license).unwrap_or(LicenseKind::Mit)
|
||||
}
|
||||
@@ -16,6 +16,7 @@ mod detail;
|
||||
mod hub;
|
||||
mod labels;
|
||||
mod legacy;
|
||||
mod license;
|
||||
mod profile;
|
||||
mod projects;
|
||||
mod providers;
|
||||
|
||||
Reference in new issue
Block a user