Skip to content
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ edition = "2024"
homepage = "https://github.com/acceldata-io/git_sync"
license = "Apache-2.0"
name = "git_sync"
version = "1.0.0"
version = "1.0.1"


[dependencies]
Expand Down
15 changes: 14 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -376,6 +376,19 @@ $ git_sync release create --current-release v1.0.1 --previous-release v0.99.6 --
```

You can also optionally specify the release name by setting the `--release-name <RELEASE NAME>` flag. If you don't specify it, it will use the name of the `--current-release` tag.

There are times where you may want to create a release for components where some do not have a previous release tag. To create a release and generate release notes for components that have a previous release and create a new release without release notes for those that don't, you can use the `--skip-missing-tag` flag. This will create releases for all components, but only generate release notes for those that have a previous release tag.

```bash
$ git_sync release create --current-release v1.0.1 --previous-release v0.99.6 --all --skip-missing-tag
```

When using `--skip-missing-tag`, you do not need to provide the `--previous-release` flag.
```bash
$ git_sync release create --current-release v1.0.1 --all --skip-missing-tag
```

You will still get an error if the current tag does not exist, but whether your repository has the `previous-release` tag will no longer matter.
### Creating and merging pull requests
You can create and merge pull requests using the `pr open` command. There are many optional flags that can be used to customize the pull request, and if you would like to see all of them, run `git_sync pr open --help`.

Expand All @@ -385,7 +398,7 @@ You can specify the most recent commit in your feature branch with the `--sha` f

Automatic merging requires specifying the `--merge` option. If you leave it out, there will be no attempt to merge the pull request automatically. Optionally, you can specify `--delete` when `--merge` is specified to automatically delete the branch after a successful merge. If the merge fails, the branch will not be deleted.

```shell
```bash
$ git_sync pr open -r https://github.com/my-org/my-repo --base main --head my_feature_branch --merge
$ git_sync pr open --all --base MY_MAIN_BRANCH --head my_feature_branch --merge
```
Expand Down
8 changes: 6 additions & 2 deletions src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -583,8 +583,8 @@ pub struct CreateReleaseCommand {
#[arg(short, long)]
pub current_release: String,
/// The previous release tag. This is used to generate the changelog
#[arg(short, long)]
pub previous_release: String,
#[arg(short, long, required_unless_present = "skip_missing_tag")]
Copy link

Copilot AI Nov 26, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The CLI allows previous_release to be omitted when skip_missing_tag is true (via required_unless_present), but the documentation example (README.md line 383) still shows both flags being used together. Based on the PR description indicating that "some repositories do not have a valid 'previous' tag," the intention seems to be handling missing tags in specific repos, not omitting the previous release parameter entirely.

When previous_release is None and defaults to an empty string (line 553), it will be passed to generate_release_notes() which attempts to fetch a tag with an empty name, causing unclear failures. Consider either:

  1. Keeping previous_release as required (removing required_unless_present), or
  2. Handling empty/None previous_release explicitly in the release functions by skipping tag comparison entirely when it's not provided.
Suggested change
#[arg(short, long, required_unless_present = "skip_missing_tag")]
#[arg(short, long, required = true)]

Copilot uses AI. Check for mistakes.
pub previous_release: Option<String>,
/// The repository for which to create the release
#[arg(short, long)]
pub repository: Option<String>,
Expand All @@ -597,6 +597,10 @@ pub struct CreateReleaseCommand {
/// Create this release for all configured repositories
#[arg(short, long, default_value_t = false)]
pub all: bool,
/// Continue creating the release even if the `previous` tag does not exist.
/// This will mean no release notes are generated.
#[arg(long, default_value_t = false)]
pub skip_missing_tag: bool,
}

// --- Backup Commands ---
Expand Down
4 changes: 2 additions & 2 deletions src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -525,7 +525,7 @@ pub fn octocrab_error_info(e: &octocrab::Error) -> (Option<http::StatusCode>, St
match e {
octocrab::Error::GitHub { source, .. } => {
let status = source.status_code;
let mut message = source.message.to_string();
let mut message = source.message.clone();
if let Some(errors) = &source.errors {
for err in errors {
if let Some(obj) = err.as_object()
Expand All @@ -536,7 +536,7 @@ pub fn octocrab_error_info(e: &octocrab::Error) -> (Option<http::StatusCode>, St
}
}
}
(Some(status), message.to_string())
(Some(status), message)
}
octocrab::Error::Http { source, .. } => (None, source.to_string()),
octocrab::Error::UriParse { source, .. } => (None, source.to_string()),
Expand Down
8 changes: 4 additions & 4 deletions src/github/backup.rs
Original file line number Diff line number Diff line change
Expand Up @@ -327,7 +327,7 @@ impl GithubClient {
if let Some(ref progress) = progress {
progress.println(format!("Failed to back up repository: {repo}"));
}
errors.push((repo.to_string(), e));
errors.push((repo.clone(), e));
}
}
}
Expand Down Expand Up @@ -512,7 +512,7 @@ impl GithubClient {
bucket.as_ref()
);

let string_name = name.to_string();
let string_name = name.clone();
let base_name = string_name.split('/').next_back().unwrap_or("");
let message = format!("\t• Uploaded backup {base_name} to {s3_url}");

Expand Down Expand Up @@ -572,10 +572,10 @@ impl GithubClient {
}
let is_tty = self.is_tty;
let output_type: Arc<OnceCell<OutputMode>> = Arc::clone(&self.output);
let key = key_name.to_string();
let key = key_name.clone();

futures.push(tokio::spawn(async move {
let bucket = bucket.to_string();
let bucket = bucket.clone();
let _lock = lock;
let response = client
.upload_part()
Expand Down
20 changes: 10 additions & 10 deletions src/github/branch.rs
Original file line number Diff line number Diff line change
Expand Up @@ -504,7 +504,7 @@ impl GithubClient {
let build_number = new_text
.split('-')
.next_back()
.ok_or_else(|| GitError::InvalidBigtopVersion(new_text.to_string()))?;
.ok_or_else(|| GitError::InvalidBigtopVersion(new_text.clone()))?;

replace_all_in_directory(
&repo_dir,
Expand All @@ -519,12 +519,12 @@ impl GithubClient {

let dir = format!("{}/bigtop-packages/src/deb", repo_dir.display());

let old_text_dash = &old_text.to_string().replace('.', "-");
let new_text_dash = &new_text.to_string().replace('.', "-");
let old_text_dash = old_text.replace('.', "-");
let new_text_dash = new_text.replace('.', "-");

let bigtop_re = Regex::new(&format!(
r"^(.*)-{}([.-].*)(.*)$",
fancy_regex::escape(old_text_dash)
fancy_regex::escape(&old_text_dash)
))
.expect("Failed to compile old version regex");

Expand All @@ -548,7 +548,7 @@ impl GithubClient {
}
}
let commit_message = if let Some(msg) = message {
msg.to_string()
msg.clone()
} else if is_version {
format!("[Automated] Changed version from '{old_text}' to '{new_text}'")
} else {
Expand Down Expand Up @@ -634,14 +634,14 @@ impl GithubClient {
) -> Result<(), GitError> {
let mut futures = FuturesUnordered::new();
for repo in repositories {
let branch = branch.to_string();
let old_text = old_text.to_string();
let new_text = new_text.to_string();
let branch = branch.clone();
let old_text = old_text.clone();
let new_text = new_text.clone();
let message = message.clone();
futures.push(async move {
let result = self
.modify_branch(
repo.to_string(),
repo.clone(),
branch,
old_text,
new_text,
Expand All @@ -658,7 +658,7 @@ impl GithubClient {
let mut errors: Vec<(String, GitError)> = Vec::new();
while let Some((repo, result)) = futures.next().await {
if result.is_err() {
errors.push((repo.to_string(), result.err().unwrap()));
errors.push((repo.clone(), result.err().unwrap()));
}
}
if !errors.is_empty() {
Expand Down
8 changes: 4 additions & 4 deletions src/github/fork.rs
Original file line number Diff line number Diff line change
Expand Up @@ -216,7 +216,7 @@ impl GithubClient {
match (parent_date, fork_date) {
(Ok(parent), Ok(fork)) => {
if parent > fork {
branches_to_sync.push(branch.to_string());
branches_to_sync.push(branch.clone());
}
}
(Err(e), _) | (_, Err(e)) => {
Expand Down Expand Up @@ -402,15 +402,15 @@ impl GithubClient {
} else {
eprintln!("Skipping branch {branch} due to non-fast-forward merge");
errors.push(GitError::GitFFMergeError {
branch: branch.to_string(),
repository: ssh_url.to_string(),
branch: branch.clone(),
repository: ssh_url.clone(),
});
}
}

// If no branches were synced, and there are errors, then we return an error
if successful == 0 && !errors.is_empty() {
return Err(GitError::GitPushError(ssh_url.to_string()));
return Err(GitError::GitPushError(ssh_url.clone()));
}

Command::new("git")
Expand Down
20 changes: 11 additions & 9 deletions src/github/match_args.rs
Original file line number Diff line number Diff line change
Expand Up @@ -326,7 +326,7 @@ async fn match_branch_cmds(
} else if let Some(repository) = &repository {
client
.modify_branch(
repository.to_string(),
repository.clone(),
branch,
old_text,
new_text,
Expand Down Expand Up @@ -372,7 +372,7 @@ async fn match_repo_cmds(
if let Some(parent) = fork_workaround.get(repository) {
client.sync_with_upstream(repository, parent).await?;
} else if forks_with_workaround.contains_key(repository) {
return Err(GitError::NoUpstreamRepo(repository.to_string()));
return Err(GitError::NoUpstreamRepo(repository.clone()));
} else if recursive {
client.sync_fork_recursive(repository).await?;
} else {
Expand Down Expand Up @@ -420,7 +420,7 @@ async fn match_repo_cmds(

let branches: Vec<Vec<String>> = branches
.iter()
.map(|(b, d)| vec![b.to_string(), d.to_string()])
.map(|(b, d)| vec![b.clone(), d.clone()])
.collect();
client.display_check_results(
vec!["Branch".to_string(), "Date".to_string()],
Expand Down Expand Up @@ -452,7 +452,7 @@ async fn match_repo_cmds(
} = &result.clone();
let branches = branches
.iter()
.map(|(b, d)| vec![b.to_string(), d.to_string()])
.map(|(b, d)| vec![b.clone(), d.clone()])
.collect();
if !client.is_tty {
println!("Repository,Branch,Date,License,Rules");
Expand Down Expand Up @@ -549,22 +549,26 @@ async fn match_release_cmds(
match cmd {
ReleaseCommand::Create(create_cmd) => {
let repository = create_cmd.repository.as_ref();
let skip_missing_previous_tag = create_cmd.skip_missing_tag;
let previous_release = create_cmd.previous_release.clone().unwrap_or_default();
Copy link

Copilot AI Nov 26, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nitpick] Using .unwrap_or_default() on previous_release results in an empty string "" being passed to the release creation functions when no previous release is specified. While this works because the error is caught and handled when skip_missing_tag is true, it would be clearer to use a more explicit empty tag placeholder like "NONE" or to handle the Option<String> directly throughout the call chain. This would make the code's intent more obvious and avoid attempting to fetch a tag with an empty name.

Copilot uses AI. Check for mistakes.
if create_cmd.all {
client
.create_all_releases(
&create_cmd.current_release,
&create_cmd.previous_release,
&previous_release,
create_cmd.release_name.as_deref(),
repos,
skip_missing_previous_tag,
)
.await?;
} else if let Some(repository) = repository {
client
.create_release(
repository,
&create_cmd.current_release,
&create_cmd.previous_release,
&previous_release,
create_cmd.release_name.as_deref(),
skip_missing_previous_tag,
)
.await?;
}
Expand Down Expand Up @@ -623,9 +627,7 @@ async fn match_backup_cmds(
return Ok(());
}
} else if let Some(repository) = repository {
let repo_dist = client
.backup_repo(repository.to_string(), path, atomic)
.await?;
let repo_dist = client.backup_repo(repository.clone(), path, atomic).await?;
if dest == BackupDestination::S3 {
if let Some(bucket) = bucket {
client.backup_to_s3(&repo_dist, bucket).await?;
Expand Down
10 changes: 5 additions & 5 deletions src/github/pr.rs
Original file line number Diff line number Diff line change
Expand Up @@ -134,8 +134,8 @@ impl GithubClient {
.map(|pr| pr.number)
.ok_or_else(|| GitError::NoSuchPR {
repository: format!("{owner}/{repo}"),
head: opts.head.to_string(),
base: opts.base.to_string(),
head: opts.head.clone(),
base: opts.base.clone(),
})?,
Err(e) => {
self.append_slack_error(format!(
Expand Down Expand Up @@ -220,7 +220,7 @@ impl GithubClient {
// Copy the fields of the opts struct, except for what we need to override (namely, the
// url)
let pr_opts = CreatePrOptions {
url: repo.to_string(),
url: repo.clone(),
..opts.clone()
};
futures.push(async move {
Expand Down Expand Up @@ -251,9 +251,9 @@ impl GithubClient {
if let Some((repo, result)) = res {
match result {
Ok(pr_number) => {
pr_map.insert(repo.to_string(), pr_number);
pr_map.insert(repo.clone(), pr_number);
}
Err(e) => errors.push((repo.to_string(), e)),
Err(e) => errors.push((repo.clone(), e)),
}
}
}
Expand Down
Loading