Skip to content

Commit faaad27

Browse files
committed
Add rebase and add-conflict handling
1 parent a8b4483 commit faaad27

8 files changed

Lines changed: 466 additions & 21 deletions

File tree

CHANGELOG.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,22 @@ All notable changes to this project will be documented in this file.
44

55
## [Unreleased]
66

7+
## [0.1.17] - 2026-06-07
8+
9+
### Added
10+
11+
- Added `rpack rebase` for rewriting existing packages against the current target repository working tree and producing a new package.
12+
- Added conflict policy options for already-present added files in `check` and `apply`:
13+
- `--resolve-added-file-conflicts <abort|skip|modify|overwrite>`
14+
- `--allow-existing-added-files <same>`
15+
- `as-modify` is accepted as an alias of `modify`.
16+
- Added automatic `ADD`-to-`MODIFY` rewrite during package application when a new-file patch conflicts with an existing file and policy allows conversion.
17+
18+
### Changed
19+
20+
- Rebase and add-conflict handling are now exposed and documented in CLI usage and README.
21+
- `rpack` still does not alter git history; `rebase` rewrites patch content only.
22+
723
## [0.1.16] - 2026-06-07
824

925
### Added

README.md

Lines changed: 23 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,13 @@
66

77
**Repository Pack**: portable, validated patch packages for Git working trees.
88

9-
`rpack` is a small CLI tool for moving code changes between repositories as a single `.rpack` file. It does not create commits, branches, tags, rebases, or otherwise modify Git history. It only validates and applies patches to the working tree.
9+
`rpack` is a small CLI tool for moving code changes between repositories as a single `.rpack` file. It does not create commits, branches, or otherwise modify Git history. It only validates and applies patches to the working tree.
1010

1111
```bash
1212
rpack create -o change.rpack
1313
rpack inspect change.rpack
1414
rpack check change.rpack
15+
rpack rebase change.rpack
1516
rpack apply change.rpack
1617
rpack undo
1718
```
@@ -38,7 +39,7 @@ Implemented:
3839
- `.rpack` as ZIP
3940
- `manifest.json`
4041
- one or more ordered `git diff --binary` patches
41-
- `create`, `inspect`, `check`, `apply`, `undo`, `history`
42+
- `create`, `inspect`, `check`, `rebase`, `apply`, `undo`, `history`
4243
- `open` for guided package application
4344
- checksum verification
4445
- clean working tree requirement by default
@@ -90,7 +91,7 @@ dotnet pack src/Rpack.Cli -c Release
9091
Build Windows MSI locally (both variants at once):
9192

9293
```powershell
93-
.\scripts\build-windows-msi.ps1 -Version 0.1.16
94+
.\scripts\build-windows-msi.ps1 -Version 0.1.17
9495
```
9596

9697
This creates two installers:
@@ -101,8 +102,8 @@ This creates two installers:
101102
Build only one variant when needed:
102103

103104
```powershell
104-
.\scripts\build-windows-msi.ps1 -Version 0.1.16 -Variant framework-dependent
105-
.\scripts\build-windows-msi.ps1 -Version 0.1.16 -Variant self-contained
105+
.\scripts\build-windows-msi.ps1 -Version 0.1.17 -Variant framework-dependent
106+
.\scripts\build-windows-msi.ps1 -Version 0.1.17 -Variant self-contained
106107
```
107108

108109
## Usage
@@ -127,6 +128,12 @@ Create a package from a revision range, still as a working tree patch:
127128
rpack create --from HEAD~2 --to HEAD -o change.rpack
128129
```
129130

131+
Rebase a package against another repository working tree:
132+
133+
```bash
134+
rpack rebase change.rpack ./other-repo -o change-rebased.rpack
135+
```
136+
130137
Inspect a package without applying it:
131138

132139
```bash
@@ -185,6 +192,17 @@ Apply a package to the current repository:
185192
rpack apply change.rpack
186193
```
187194

195+
Handle existing-file add conflicts explicitly when needed:
196+
197+
```bash
198+
rpack apply change.rpack --allow-existing-added-files modify
199+
rpack apply change.rpack --resolve-added-file-conflicts as-modify
200+
rpack apply change.rpack --allow-existing-added-files skip
201+
rpack apply change.rpack --allow-existing-added-files overwrite
202+
```
203+
204+
By default, `rpack` fails on added-file conflicts (`abort`).
205+
188206
Open a package with a guided inspect/check/apply flow:
189207

190208
```bash

scripts/build-windows-msi.ps1

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
param(
2-
[string] $Version = "0.1.16",
2+
[string] $Version = "0.1.17",
33
[string] $Runtime = "win-x64",
44
[string] $Configuration = "Release",
55
[string] $OutputDirectory = "artifacts",

src/Rpack.Cli/Program.cs

Lines changed: 45 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
"diagnose" => RunDiagnose(args.Skip(1).ToArray(), service),
2828
"lint" => RunLint(args.Skip(1).ToArray(), service),
2929
"apply" => RunApply(args.Skip(1).ToArray(), service),
30+
"rebase" => RunRebase(args.Skip(1).ToArray(), service),
3031
"open" => RunOpen(args.Skip(1).ToArray(), service, gitClient),
3132
"undo" => RunUndo(args.Skip(1).ToArray(), service),
3233
"history" => RunHistory(args.Skip(1).ToArray(), service),
@@ -142,6 +143,34 @@ static int RunCheck(string[] args, RpackPackageService service)
142143
AllowDirty = options.Has("--allow-dirty"),
143144
StrictBase = options.Has("--strict-base"),
144145
PathPrefix = options.Value("--path-prefix"),
146+
AddedFileConflictResolution = ResolveAddedFileConflictResolution(options),
147+
IgnoreSpaceChange = ResolveIgnoreSpaceChange(options)
148+
});
149+
150+
return PrintResult(result);
151+
}
152+
153+
static int RunRebase(string[] args, RpackPackageService service)
154+
{
155+
var options = CliOptions.Parse(args);
156+
if (options.Positionals.Count < 1)
157+
{
158+
return Fail("Usage: rpack rebase <package.rpack> [repo] -o <package-rebased.rpack>");
159+
}
160+
161+
var output = options.Value("-o") ?? options.Value("--output");
162+
if (string.IsNullOrWhiteSpace(output))
163+
{
164+
return Fail("Missing output path. Use -o package-rebased.rpack.");
165+
}
166+
167+
var result = service.Rebase(new RebasePackageOptions
168+
{
169+
PackagePath = options.Positionals[0],
170+
RepositoryPath = ResolveRepositoryArgument(options),
171+
OutputPath = output,
172+
PathPrefix = options.Value("--path-prefix"),
173+
AddedFileConflictResolution = ResolveAddedFileConflictResolution(options),
145174
IgnoreSpaceChange = ResolveIgnoreSpaceChange(options)
146175
});
147176

@@ -163,6 +192,7 @@ static int RunApply(string[] args, RpackPackageService service)
163192
AllowDirty = options.Has("--allow-dirty"),
164193
StrictBase = options.Has("--strict-base"),
165194
PathPrefix = options.Value("--path-prefix"),
195+
AddedFileConflictResolution = ResolveAddedFileConflictResolution(options),
166196
IgnoreSpaceChange = ResolveIgnoreSpaceChange(options)
167197
});
168198

@@ -184,6 +214,7 @@ static int RunDiagnose(string[] args, RpackPackageService service)
184214
AllowDirty = options.Has("--allow-dirty"),
185215
StrictBase = options.Has("--strict-base"),
186216
PathPrefix = options.Value("--path-prefix"),
217+
AddedFileConflictResolution = ResolveAddedFileConflictResolution(options),
187218
IgnoreSpaceChange = ResolveIgnoreSpaceChange(options)
188219
});
189220

@@ -261,6 +292,7 @@ static int RunOpen(string[] args, RpackPackageService service, GitClient gitClie
261292
AllowDirty = allowDirty,
262293
StrictBase = strictBase,
263294
PathPrefix = pathPrefix,
295+
AddedFileConflictResolution = ResolveAddedFileConflictResolution(options),
264296
AllowedDirtyPaths = allowedDirtyPaths,
265297
IgnoreSpaceChange = ignoreSpaceChange
266298
});
@@ -446,8 +478,9 @@ rpack create --from <rev> --to <rev> -o <package.rpack> [--repo <repo>]
446478
rpack inspect <package.rpack> [--path-prefix <prefix>]
447479
rpack check <package.rpack> [repo] [--allow-dirty] [--strict-base] [--path-prefix <prefix>] [--strict]
448480
rpack diagnose <package.rpack> [repo] [--allow-dirty] [--strict-base] [--path-prefix <prefix>] [--strict]
481+
rpack rebase <package.rpack> [repo] -o <package-rebased.rpack> [--path-prefix <prefix>] [--strict]
449482
rpack lint <package.rpack> [--path-prefix <prefix>]
450-
rpack apply <package.rpack> [repo] [--allow-dirty] [--strict-base] [--path-prefix <prefix>] [--strict]
483+
rpack apply <package.rpack> [repo] [--allow-dirty] [--strict-base] [--path-prefix <prefix>] [--strict] [--resolve-added-file-conflicts <mode>] [--allow-existing-added-files <mode>]
451484
rpack open <package.rpack> [repo] [--allow-dirty] [--strict-base] [--path-prefix <prefix>] [--strict] [--yes]
452485
rpack undo [repo] [--allow-dirty]
453486
rpack history [repo]
@@ -465,6 +498,8 @@ rpack applies validated patch packages to the Git working tree.
465498
--strict Require exact patch context whitespace.
466499
--strict-whitespace Alias for --strict.
467500
--ignore-space-change Compatibility no-op; this is the default mode.
501+
--resolve-added-file-conflicts <mode> How to resolve add-conflict: abort|skip|modify|overwrite.
502+
--allow-existing-added-files <mode> Alias for --resolve-added-file-conflicts.
468503
""");
469504
}
470505

@@ -479,6 +514,13 @@ static bool ResolveIgnoreSpaceChange(CliOptions options)
479514
return !strictWhitespace;
480515
}
481516

517+
static string ResolveAddedFileConflictResolution(CliOptions options)
518+
{
519+
return options.Value("--resolve-added-file-conflicts")
520+
?? options.Value("--allow-existing-added-files")
521+
?? "abort";
522+
}
523+
482524
static void PrintVersion()
483525
{
484526
var version = Assembly.GetExecutingAssembly()
@@ -502,6 +544,8 @@ internal sealed class CliOptions
502544
"--id",
503545
"--title",
504546
"--description",
547+
"--resolve-added-file-conflicts",
548+
"--allow-existing-added-files",
505549
"--path-prefix"
506550
};
507551

src/Rpack.Cli/Rpack.Cli.csproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
<TargetFramework>net10.0</TargetFramework>
1010
<AssemblyName>rpack</AssemblyName>
1111
<RootNamespace>Rpack.Cli</RootNamespace>
12-
<Version>0.1.16</Version>
12+
<Version>0.1.17</Version>
1313
<IncludeSourceRevisionInInformationalVersion>false</IncludeSourceRevisionInInformationalVersion>
1414
<Authors>Pawel Potepa</Authors>
1515
<Description>Portable, validated patch packages for Git working trees.</Description>

src/Rpack.Core/GitClient.cs

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -180,6 +180,17 @@ public string CreateDiff(string repositoryPath, string fromRevision, string toRe
180180
return result.StandardOutput;
181181
}
182182

183+
public string CreateNoIndexDiff(string repositoryPath, string leftPath, string rightPath)
184+
{
185+
var result = _processRunner.Run("git", ["diff", "--binary", "--no-index", "--", leftPath, rightPath], repositoryPath);
186+
if (!result.Success && result.ExitCode != 1)
187+
{
188+
throw new InvalidOperationException(result.CombinedOutput);
189+
}
190+
191+
return result.StandardOutput;
192+
}
193+
183194
public string CreateWorkingTreeDiff(string repositoryPath)
184195
{
185196
var result = _processRunner.Run("git", ["diff", "--binary"], repositoryPath);
@@ -287,6 +298,44 @@ public RpackResult ReverseApply(string repositoryPath, string patchPath, bool ig
287298
? RpackResult.Ok("Patch reverted.")
288299
: RpackResult.Fail(result.CombinedOutput);
289300
}
301+
302+
public string CreateDetachedWorktree(string repositoryPath)
303+
{
304+
var temporaryWorktreePath = Path.Combine(Path.GetTempPath(), $"rpack-worktree-{Guid.NewGuid():N}");
305+
Directory.CreateDirectory(temporaryWorktreePath);
306+
var result = _processRunner.Run("git", ["worktree", "add", "--detach", temporaryWorktreePath, "HEAD"], repositoryPath);
307+
if (!result.Success)
308+
{
309+
Directory.Delete(temporaryWorktreePath, recursive: true);
310+
throw new InvalidOperationException(result.CombinedOutput);
311+
}
312+
313+
return temporaryWorktreePath;
314+
}
315+
316+
public RpackResult RemoveDetachedWorktree(string repositoryPath, string worktreePath)
317+
{
318+
if (string.IsNullOrWhiteSpace(worktreePath))
319+
{
320+
return RpackResult.Ok("No worktree path provided.");
321+
}
322+
323+
var result = _processRunner.Run("git", ["worktree", "remove", "--force", worktreePath], repositoryPath);
324+
if (!result.Success)
325+
{
326+
try
327+
{
328+
Directory.Delete(worktreePath, recursive: true);
329+
return RpackResult.Ok("Worktree removed.");
330+
}
331+
catch
332+
{
333+
return RpackResult.Fail(result.CombinedOutput);
334+
}
335+
}
336+
337+
return RpackResult.Ok("Worktree removed.");
338+
}
290339
}
291340

292341
public sealed record GitRepository(string RootPath, string StatePath);

0 commit comments

Comments
 (0)