Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
24 changes: 24 additions & 0 deletions Config/products/macpacker.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,30 @@
{
"version": "1.0.1",
"items": [
{
"type": "feat",
"title": {
"en": "Skip gitignored files when compressing folders",
"zh-Hans": "压缩文件夹时跳过 .gitignore 忽略的文件",
"fr": "Ignorer les fichiers exclus par .gitignore lors de la compression",
"de": "Beim Komprimieren von .gitignore ignorierte Dateien auslassen",
"it": "Salta i file ignorati da .gitignore comprimendo cartelle",
"ja": "フォルダ圧縮時に .gitignore 対象ファイルを除外",
"fa": "نادیده گرفتن فایل‌های مستثناشده با .gitignore هنگام فشرده‌سازی پوشه‌ها",
"pl": "Pomijaj pliki ignorowane przez .gitignore przy kompresji",
"pt-BR": "Ignorar arquivos indicados pelo .gitignore ao compactar pastas",
"ru": "Пропускать файлы, игнорируемые .gitignore, при сжатии папок",
"uk": "Пропускати файли, ігноровані .gitignore, під час стиснення тек",
"es-MX": "Omitir archivos ignorados por .gitignore al comprimir carpetas",
"ko": "폴더 압축 시 .gitignore에서 무시한 파일 제외",
"nl": "Door .gitignore genegeerde bestanden overslaan bij comprimeren",
"tr": "Klasör sıkıştırırken .gitignore'ın yok saydığı dosyaları atla",
"vi": "Bỏ qua tệp bị .gitignore loại trừ khi nén thư mục"
},
"issues": [
"237"
]
},
{
"type": "fix",
"highlight": true,
Expand Down
7 changes: 6 additions & 1 deletion MacPacker/Core/UrlHandling/AppUrlCompressHandler.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import AppKit
import Core
import FinderMenu
import Foundation
import Swift7zip
import tb

private let log = tb.Logger(subsystem: "app.MacPacker", category: "url")
Expand All @@ -25,7 +26,11 @@ private func writeArchive(
) async -> Bool {
log.notice("Compressing \(items.count) item(s) to \(destination.lastPathComponent)")
let state = ArchiveState(catalog: catalog, engineSelector: engineSelector)
await state.compress(items, to: destination)
let options = CompressionOptions(
format: destination.pathExtension.lowercased() == "7z" ? .sevenZ : .zip,
respectGitIgnore: UserDefaults.standard.bool(forKey: Keys.respectGitIgnore)
)
await state.compress(items, to: destination, options: options)

if let error = state.error {
log.error("Compress failed", context: ["error": error])
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,12 @@ struct SaveOptionsRows: View {
.labelsHidden()
.accessibilityIdentifier("saveOptions.excludeDSStore")
}
row(Text("Skip Git-ignored project files", comment: "Setting to omit files matched by a folder's .gitignore when compressing")) {
Toggle(isOn: $options.respectGitIgnore) { EmptyView() }
.labelsHidden()
.accessibilityIdentifier("saveOptions.respectGitIgnore")
.help(Text("If a folder has a .gitignore file, skip the files and folders it says to ignore.", comment: "Explains the Git-ignored project files setting"))
}
}
}

Expand Down
16 changes: 16 additions & 0 deletions MacPacker/Features/Settings/GeneralSettingsView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ struct GeneralSettingsView: View {
@AppStorage(Keys.showMenuBarItem) var showMenuBarItem: Bool = false
@AppStorage(Keys.rememberRecentArchives) var rememberRecentArchives: Bool = true
@AppStorage(Keys.smartExtraction, store: .macPackerShared) var smartExtraction: Bool = true
@AppStorage(Keys.respectGitIgnore) var respectGitIgnore: Bool = false

var body: some View {
VStack(spacing: 8) {
Expand Down Expand Up @@ -119,6 +120,21 @@ struct GeneralSettingsView: View {
.padding(.leading, 8)
.frame(width: 240, alignment: .leading)
}
HStack(alignment: .top) {
Text("Skip Git-ignored project files", comment: "Setting to omit files matched by a folder's .gitignore when compressing")
.frame(width: 200, alignment: .trailing)
VStack(alignment: .leading, spacing: 4) {
Toggle(isOn: $respectGitIgnore) { EmptyView() }
.labelsHidden()
.accessibilityIdentifier("settings.respectGitIgnore")
Text("If a folder has a .gitignore file, skip the files and folders it says to ignore.", comment: "Explains the Git-ignored project files setting")
.font(.caption)
.foregroundStyle(.secondary)
.fixedSize(horizontal: false, vertical: true)
}
.padding(.leading, 8)
.frame(width: 240, alignment: .leading)
}
}
.padding()
// Turning it off is a privacy switch, so what was collected goes with it —
Expand Down
8 changes: 8 additions & 0 deletions MacPacker/Localizable.xcstrings
Original file line number Diff line number Diff line change
@@ -1,6 +1,14 @@
{
"sourceLanguage": "en",
"strings": {
"If a folder has a .gitignore file, skip the files and folders it says to ignore.": {
"comment": "Explains the Git-ignored project files setting",
"extractionState": "manual"
},
"Skip Git-ignored project files": {
"comment": "Setting to omit files matched by a folder's .gitignore when compressing",
"extractionState": "manual"
},
"appExtractionInProgress": {
"comment": "Title of the alert shown when the user quits while an extraction is running.",
"extractionState": "manual",
Expand Down
2 changes: 2 additions & 0 deletions Modules/Sources/Core/AppStorageKeys.swift
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,8 @@ public enum Keys {
/// Leave `.DS_Store` files out of archives. Off by default, as in Finder's
/// own Compress.
public static let saveOptionsExcludeDSStore = "saveOptionsExcludeDSStore"
/// Skip additions matched by `.gitignore` when compressing folders.
public static let respectGitIgnore = "respectGitIgnore"

// register defaults upon app start so that the archive table has a default it
// can use when showing the table for the first time
Expand Down
24 changes: 19 additions & 5 deletions Modules/Sources/Core/ArchiveState.swift
Original file line number Diff line number Diff line change
Expand Up @@ -548,6 +548,11 @@ extension ArchiveState {
/// is left out, with the reason in `error`, and so is one inside it.
@discardableResult
public func add(url: URL) -> Bool {
add(url: url, ignoring: nil)
}

@discardableResult
private func add(url: URL, ignoring gitIgnore: GitIgnoreFilter?) -> Bool {
guard !isSaving else {
log.notice("Ignoring add — a save is in progress", context: ["file": url.lastPathComponent])
return false
Expand All @@ -569,7 +574,8 @@ extension ArchiveState {
// drop or a panel, a grant reused from storage gives no access
// outside it (#278).
added = Sandbox.accessSync(url: url) {
addFolder(url: url, archivePath: base + url.lastPathComponent, under: selectedItem)
addFolder(url: url, archivePath: base + url.lastPathComponent,
under: selectedItem, ignoring: gitIgnore)
}
} else {
addFile(url: url, archivePath: base + url.lastPathComponent, under: selectedItem)
Expand All @@ -590,8 +596,9 @@ extension ArchiveState {
/// for a single folder, an empty one (#278).
public func compress(_ items: [URL], to destination: URL, options: CompressionOptions? = nil) async {
create()
let gitIgnore = options?.respectGitIgnore == true ? GitIgnoreFilter(paths: items) : nil
for item in items {
guard add(url: item) else { return }
guard add(url: item, ignoring: gitIgnore) else { return }
}
await save(to: destination, options: options)?.value
}
Expand All @@ -617,7 +624,8 @@ extension ArchiveState {
}

/// - Returns: whether `url` and every folder below it could be read.
private func addFolder(url: URL, archivePath: String, under parent: ArchiveItem) -> Bool {
private func addFolder(url: URL, archivePath: String, under parent: ArchiveItem,
ignoring gitIgnore: GitIgnoreFilter? = nil) -> Bool {
// Read the contents first: if the folder can't be enumerated we must not
// add it as an empty directory (that would silently drop its real
// contents on save). Skip it and surface the error instead.
Expand Down Expand Up @@ -654,8 +662,15 @@ extension ArchiveState {

var complete = true
for child in children.sorted(by: { $0.lastPathComponent < $1.lastPathComponent }) {
do {
if try gitIgnore?.isIgnored(child, directory: child.isDirectory) == true { continue }
} catch {
self.error = error.localizedDescription
return false
}
if child.isDirectory {
complete = addFolder(url: child, archivePath: archivePath + "/" + child.lastPathComponent, under: item)
complete = addFolder(url: child, archivePath: archivePath + "/" + child.lastPathComponent,
under: item, ignoring: gitIgnore)
&& complete
} else {
addFile(url: child, archivePath: archivePath + "/" + child.lastPathComponent, under: item)
Expand Down Expand Up @@ -1710,4 +1725,3 @@ extension ArchiveState {
return adjustedSelection
}
}

23 changes: 21 additions & 2 deletions Modules/Sources/Core/Settings/ArchiveSaveOptions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -87,9 +87,25 @@ public final class ArchiveSaveOptions: ObservableObject {
/// Leave `.DS_Store` files out. Not per format: it is about the files, not
/// the archive.
@Published public var excludeDSStore: Bool { didSet { autosave() } }
/// Shared with Finder compression and General Settings, rather than per
/// format: the same folder should package the same way as zip or 7z.
public var respectGitIgnore: Bool {
get { pendingRespectGitIgnore ?? defaults.bool(forKey: Keys.respectGitIgnore) }
set {
objectWillChange.send()
if storage == .quickCompress {
defaults.set(newValue, forKey: Keys.respectGitIgnore)
} else {
// A save panel's choices take effect only when Save goes ahead.
// Cancel must not alter the global Finder preference.
pendingRespectGitIgnore = newValue
}
}
}

private let defaults: UserDefaults
private let storage: Storage
private var pendingRespectGitIgnore: Bool?
private var remembered: [Format: Remembered] = [:]
/// Set while a format's settings are taken over, which changes one property
/// after the other: storing each step would store half of one format.
Expand Down Expand Up @@ -161,13 +177,14 @@ public final class ArchiveSaveOptions: ObservableObject {
wordSize: compresses ? wordSize : nil,
solidBlockSize: compresses && hasSolidBlocks && (solidBlockSize ?? 0) > 0 ? solidBlockSize : nil,
volumeSize: volumeSize,
excludeDSStore: excludeDSStore)
excludeDSStore: excludeDSStore,
respectGitIgnore: respectGitIgnore)
}

/// What the start page's drop area writes: the format picked there, defaults
/// for the rest. It shows no other option, so no password or volume size set
/// for Quick Compress may reach it.
public var startPageOptions: CompressionOptions { .init(format: format) }
public var startPageOptions: CompressionOptions { .init(format: format, respectGitIgnore: respectGitIgnore) }

// MARK: Memory

Expand All @@ -183,6 +200,8 @@ public final class ArchiveSaveOptions: ObservableObject {
}
defaults.set(format.rawValue, forKey: storage.formatKey)
defaults.set(excludeDSStore, forKey: storage.excludeDSStoreKey)
defaults.set(respectGitIgnore, forKey: Keys.respectGitIgnore)
pendingRespectGitIgnore = nil
}

private func autosave() {
Expand Down
8 changes: 7 additions & 1 deletion Modules/Sources/Swift7zip/CompressionOptions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,10 @@ public struct CompressionOptions: Sendable {
/// anyone means to archive. Entries the archive holds already stay.
public var excludeDSStore: Bool

/// Leave out files matched by a folder's `.gitignore` when adding files
/// from disk. Entries already in an archive are not changed.
public var respectGitIgnore: Bool

/// Whether this writes an encrypted archive.
public var encrypts: Bool { !(password ?? "").isEmpty }

Expand All @@ -170,7 +174,8 @@ public struct CompressionOptions: Sendable {
wordSize: UInt32? = nil,
solidBlockSize: UInt64? = nil,
volumeSize: UInt64? = nil,
excludeDSStore: Bool = false
excludeDSStore: Bool = false,
respectGitIgnore: Bool = false
) {
self.format = format
self.level = level
Expand All @@ -184,5 +189,6 @@ public struct CompressionOptions: Sendable {
self.solidBlockSize = solidBlockSize
self.volumeSize = volumeSize
self.excludeDSStore = excludeDSStore
self.respectGitIgnore = respectGitIgnore
}
}
Loading