|
| 1 | +// |
| 2 | +// Apply.swift |
| 3 | +// AuroraEditor |
| 4 | +// |
| 5 | +// Created by Nanashi Li on 2022/08/15. |
| 6 | +// Copyright © 2022 Aurora Company. All rights reserved. |
| 7 | +// This source code is restricted for Aurora Editor usage only. |
| 8 | +// |
| 9 | + |
| 10 | +import Foundation |
| 11 | + |
| 12 | +public struct Apply { |
| 13 | + |
| 14 | + /// Applies changes to the index for a specified file in a git repository, with special handling for renamed files. |
| 15 | + /// |
| 16 | + /// If the file change represents a rename, the function reconstructs this rename in the index. This involves |
| 17 | + /// staging the old file path for update and determining the blob ID of the removed file to update the index. |
| 18 | + /// This process ensures that the rename is properly reflected in the index, preparing it for a commit. |
| 19 | + /// |
| 20 | + /// - Parameters: |
| 21 | + /// - directoryURL: The URL of the directory containing the git repository. |
| 22 | + /// - file: A `WorkingDirectoryFileChange` object representing the file whose changes are to be applied to the index. |
| 23 | + /// - Throws: An error if the git commands fail to execute. |
| 24 | + /// |
| 25 | + /// # Example: |
| 26 | + /// ``` |
| 27 | + /// let directoryURL = URL(fileURLWithPath: "/path/to/repo") |
| 28 | + /// let fileChange = WorkingDirectoryFileChange(path: "newName.txt", status: .renamed(oldName: "oldName.txt")) |
| 29 | + /// try await applyPatchToIndex(directoryURL: directoryURL, file: fileChange) |
| 30 | + /// ``` |
| 31 | + func applyPatchToIndex(directoryURL: URL, |
| 32 | + file: WorkingDirectoryFileChange) throws { |
| 33 | + // If the file was a rename we have to recreate that rename since we've |
| 34 | + // just blown away the index. |
| 35 | + if file.status.kind == .renamed { |
| 36 | + if let renamedFile = file.status as? CopiedOrRenamedFileStatus { |
| 37 | + if renamedFile.kind == .renamed { |
| 38 | + try GitShell().git(args: ["add", "--u", "--", renamedFile.oldPath], |
| 39 | + path: directoryURL, |
| 40 | + name: #function) |
| 41 | + |
| 42 | + // Figure out the blob oid of the removed file |
| 43 | + let oldFile = try GitShell().git(args: ["ls-tree", "HEAD", "--", renamedFile.oldPath], |
| 44 | + path: directoryURL, |
| 45 | + name: #function) |
| 46 | + |
| 47 | + let info = oldFile.stdout.split(separator: "\t", |
| 48 | + maxSplits: 1, |
| 49 | + omittingEmptySubsequences: true)[0] |
| 50 | + let components = info.split(separator: " ", |
| 51 | + maxSplits: 3, |
| 52 | + omittingEmptySubsequences: true) |
| 53 | + let mode = components[0] |
| 54 | + let oid = components[2] |
| 55 | + |
| 56 | + try GitShell().git(args: ["update-index", |
| 57 | + "--add", |
| 58 | + "--cacheinfo", |
| 59 | + String(mode), |
| 60 | + String(oid), |
| 61 | + file.path], |
| 62 | + path: directoryURL, |
| 63 | + name: #function) |
| 64 | + } |
| 65 | + } |
| 66 | + } |
| 67 | + |
| 68 | + let applyArgs: [String] = [ |
| 69 | + "apply", |
| 70 | + "--cached", |
| 71 | + "--unidiff-zero", |
| 72 | + "--whitespace=nowarn", |
| 73 | + "-" |
| 74 | + ] |
| 75 | + |
| 76 | + let diff = try GitDiff().getWorkingDirectoryDiff(directoryURL: directoryURL, |
| 77 | + file: file) |
| 78 | + |
| 79 | + if let diff = diff as? TextDiff { |
| 80 | + switch diff.kind { |
| 81 | + case .image, .binary, .submodule: |
| 82 | + throw NSError(domain: "PatchError", code: 0, userInfo: [NSLocalizedDescriptionKey: "Can't create partial commit in binary file: \(file.path)"]) |
| 83 | + case .unrenderable: |
| 84 | + throw NSError(domain: "PatchError", code: 1, userInfo: [NSLocalizedDescriptionKey: "File diff is too large to generate a partial commit: \(file.path)"]) |
| 85 | + default: |
| 86 | + fatalError("Unknown diff kind: \(diff.kind)") |
| 87 | + } |
| 88 | + } |
| 89 | + |
| 90 | + if let diff = diff as? TextDiff { |
| 91 | + let patch = try PatchFormatterParser().formatPatch(file: file, |
| 92 | + diff: diff) |
| 93 | + |
| 94 | + try GitShell().git(args: applyArgs, |
| 95 | + path: directoryURL, |
| 96 | + name: #function, |
| 97 | + options: IGitExecutionOptions(stdin: patch)) |
| 98 | + } |
| 99 | + } |
| 100 | + |
| 101 | + /// Verifies if a patch can be applied cleanly to the current state of the repository. |
| 102 | + /// |
| 103 | + /// This function uses the `git apply --check` command to test if the provided patch can be applied. |
| 104 | + /// The `--check` flag ensures that no changes are actually made to the files. It simply checks |
| 105 | + /// for potential conflicts. If the patch cannot be applied due to conflicts, the function returns `false`. |
| 106 | + /// |
| 107 | + /// - Parameters: |
| 108 | + /// - directoryURL: The URL of the directory containing the git repository. |
| 109 | + /// - patch: A string containing the patch data. |
| 110 | + /// - Returns: `true` if the patch can be applied cleanly, or `false` if conflicts are detected. |
| 111 | + /// - Throws: An error if the git command fails for reasons other than the patch not applying. |
| 112 | + /// |
| 113 | + /// # Example: |
| 114 | + /// ``` |
| 115 | + /// let directoryURL = URL(fileURLWithPath: "/path/to/repo") |
| 116 | + /// let patchString = "diff --git a/file.txt b/file.txt..." |
| 117 | + /// let canApplyPatch = try checkPatch(directoryURL: directoryURL, patch: patchString) |
| 118 | + /// ``` |
| 119 | + func checkPatch(directoryURL: URL, |
| 120 | + patch: String) throws -> Bool { |
| 121 | + let result = try GitShell().git(args: ["apply", "--check", "-"], |
| 122 | + path: directoryURL, |
| 123 | + name: #function, |
| 124 | + options: IGitExecutionOptions( |
| 125 | + expectedErrors: [GitError.PatchDoesNotApply] |
| 126 | + )) |
| 127 | + |
| 128 | + if result.gitError == GitError.PatchDoesNotApply { |
| 129 | + return false |
| 130 | + } |
| 131 | + |
| 132 | + // If `apply --check` succeeds, then the patch applies cleanly. |
| 133 | + return true |
| 134 | + } |
| 135 | + |
| 136 | + /// Discards selected changes from a file in the working directory of a git repository. |
| 137 | + /// |
| 138 | + /// This function generates a patch representing the inverse of the selected changes and applies it |
| 139 | + /// to the file to discard those changes. If the selection results in no changes, the function does |
| 140 | + /// nothing. The function uses the `git apply` command with flags designed to apply a patch with |
| 141 | + /// a zero context and to suppress warnings about whitespace. |
| 142 | + /// |
| 143 | + /// - Parameters: |
| 144 | + /// - directoryURL: The URL of the directory containing the git repository. |
| 145 | + /// - filePath: The path to the file within the repository from which changes will be discarded. |
| 146 | + /// - diff: An object conforming to the `ITextDiff` protocol representing the diff of the file. |
| 147 | + /// - selection: A `DiffSelection` object representing the selected changes to discard. |
| 148 | + /// - Throws: An error if the patch cannot be generated or applied. |
| 149 | + /// |
| 150 | + /// # Example: |
| 151 | + /// ``` |
| 152 | + /// let directoryURL = URL(fileURLWithPath: "/path/to/repo") |
| 153 | + /// let filePath = "file.txt" |
| 154 | + /// let diff = TextDiff(...) // Diff object conforming to ITextDiff |
| 155 | + /// let selection = DiffSelection(...) // Selection object specifying which changes to discard |
| 156 | + /// try discardChangesFromSelection(directoryURL: directoryURL, |
| 157 | + /// filePath: filePath, |
| 158 | + /// diff: diff, |
| 159 | + /// selection: selection) |
| 160 | + /// ``` |
| 161 | + func discardChangesFromSelection(directoryURL: URL, |
| 162 | + filePath: String, |
| 163 | + diff: ITextDiff, |
| 164 | + selection: DiffSelection) throws { |
| 165 | + guard let patch = PatchFormatterParser().formatPatchToDiscardChanges(filePath: filePath, |
| 166 | + diff: diff, |
| 167 | + selection: selection) else { |
| 168 | + // When the patch is null we don't need to apply it since it will be a noop. |
| 169 | + return |
| 170 | + } |
| 171 | + |
| 172 | + let args = ["apply", "--unidiff-zero", "--whitespace=nowarn", "-"] |
| 173 | + |
| 174 | + try GitShell().git(args: args, |
| 175 | + path: directoryURL, |
| 176 | + name: #function, |
| 177 | + options: IGitExecutionOptions(stdin: patch)) |
| 178 | + } |
| 179 | +} |
0 commit comments