Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Examples on usage #52

Open
gabrie30 opened this issue Oct 1, 2024 · 2 comments
Open

Examples on usage #52

gabrie30 opened this issue Oct 1, 2024 · 2 comments

Comments

@gabrie30
Copy link

gabrie30 commented Oct 1, 2024

Thank you for this useful tool! I'm new to working with patches and have been experimenting with this tool.

I'm writing a script that takes a patch as a string from one copy of a repository and applies it to another copy of the same repository. Below is a sample of my code. I'm unsure if I'm using the tool correctly or handling all the cases properly.

Does this look like the correct usage? Specifically, I'm uncertain if I should handle each case with conditions like file.IsNew, file.IsCopy, etc. Also I'm unsure if a binary can also be IsBinary and IsNew.

The script works for my basic tests, which include creating, copying, and renaming files. Haven't yet tried with binaries. However, I'm concerned that I might not be accounting for all possible scenarios that a user could perform on a repo and my patch would miss those changes.

...

type Repo struct {
  Name string
  AbsoluteRepoPath string
}

absoluteRepoPath := repo.AbsoluteRepoPath

patchReader := strings.NewReader(patchContent)
files, _, err := gitdiff.Parse(patchReader)
if err != nil {
  log.Fatalf("Failed to parse patch: %v", err)
}

if len(files) > 0 {
  log.Printf("Found %d files to process for repo: %s", len(files), repo.Name)
  for i := range files {
    file := files[i]
    if file.IsDelete {
      deletedFilePath := filepath.Join(absoluteRepoPath, file.OldName)
      if err := os.Remove(deletedFilePath); err != nil {
        log.Printf("Failed to delete file %s: %v", deletedFilePath, err)
        continue
      }
      log.Printf("Successfully deleted file: %s in repo: %s", file.OldName, repo.Name)
    } else if file.IsNew {
      newFilePath := filepath.Join(absoluteRepoPath, file.NewName)
      if err := os.MkdirAll(filepath.Dir(newFilePath), 0755); err != nil {
        log.Printf("Failed to create directories for %s: %v", newFilePath, err)
        continue
      }

      newFile, err := os.Create(newFilePath)
      if err != nil {
        log.Printf("Failed in IsNew to create new file %s: %v", newFilePath, err)
        continue
      }
      defer newFile.Close()
      log.Printf("Successfully created new file: %s in repo: %s", file.NewName, repo.Name)

      var patchedContent bytes.Buffer
      if err := gitdiff.Apply(&patchedContent, bytes.NewReader([]byte{}), file); err != nil {
        log.Printf("Failed to apply patch in IsNew to new file %s in repo %s: %v", file.NewName, repo.Name, err)
        continue
      }

      if _, err := newFile.Write(patchedContent.Bytes()); err != nil {
        log.Printf("Failed to write patched content to new file %s: %v", newFilePath, err)
        continue
      }

      log.Printf("Successfully applied IsNew, file: %s in repo: %s", file.NewName, repo.Name)
    } else if file.IsCopy {
      destPath := filepath.Join(absoluteRepoPath, file.NewName)

      if err := os.MkdirAll(filepath.Dir(destPath), 0755); err != nil {
        log.Printf("Failed to create directories for %s: %v", destPath, err)
        continue
      }
      newFile, err := os.Create(destPath)
      if err != nil {
        log.Printf("Failed in IsCopy to create new file %s: %v", destPath, err)
        continue
      }
      defer newFile.Close()

      newFileContent, err := io.ReadAll(newFile)
      if err != nil {
        log.Printf("Failed in IsCopy to read new file %s in repo %s: %v", file.NewName, repo.Name, err)
        continue
      }
      var patchedContent bytes.Buffer
      if err := gitdiff.Apply(&patchedContent, bytes.NewReader(newFileContent), file); err != nil {
        log.Printf("Failed to apply patch in IsCopy to new file %s in repo %s: %v", file.NewName, repo.Name, err)
        continue
      }
      // Write the patched content to the file
      if err := os.WriteFile(destPath, patchedContent.Bytes(), file.OldMode); err != nil {
        log.Printf("Failed in IsCopy to write patched content to new file %s: %v", destPath, err)
        continue
      }
      log.Printf("Successfully applied IsCopy, file: %s in repo: %s", file.NewName, repo.Name)
    } else if file.IsRename {
      destPath := filepath.Join(absoluteRepoPath, file.NewName)
      if err := os.MkdirAll(filepath.Dir(destPath), file.OldMode); err != nil {
        log.Printf("Failed to create directories for %s: %v", destPath, err)
        continue
      }
      originalFilePath := filepath.Join(absoluteRepoPath, file.OldName)
      if err := os.Rename(originalFilePath, destPath); err != nil {
        log.Printf("Failed to rename file from %s to %s: %v", originalFilePath, destPath, err)
        continue
      }
      log.Printf("Successfully applied IsRename, file: %s in repo: %s", file.OldName, repo.Name)
    } else if file.IsBinary {
      // TODO not sure if I have to handle every action for a binary, e.g. isRename, isNew, etc.
      // Can files even be IsBinary && isRename?
      // If so then I should handle this first
      binaryFilePath := filepath.Join(absoluteRepoPath, file.NewName)
      if err := os.MkdirAll(filepath.Dir(binaryFilePath), file.OldMode); err != nil {
        log.Printf("Failed to create directories for %s: %v", binaryFilePath, err)
        continue
      }
      if err := os.WriteFile(binaryFilePath, []byte{}, file.OldMode); err != nil {
        log.Printf("Failed to write binary content to file %s: %v", binaryFilePath, err)
        continue
      }
      log.Printf("Successfully applied IsBinary, file: %s in repo: %s", file.OldName, repo.Name)
    } else {
      currentFile := filepath.Join(absoluteRepoPath, file.NewName)
      currentFileBytes, err := os.ReadFile(currentFile)
      if err != nil {
        log.Printf("Failed to read file %s: %v", currentFile, err)
        continue
      }
      var patchedContent bytes.Buffer
      if err := gitdiff.Apply(&patchedContent, bytes.NewReader(currentFileBytes), file); err != nil {
        log.Printf("Failed to apply patch in catch all to new file %s in repo %s: %v", file.NewName, repo.Name, err)
        continue
      }
      log.Printf("Successfully applied catch all, file: %s in repo: %s", file.NewName, repo.Name)
    }
  }
} else {
  log.Printf("No files to apply patch to for repo: %s", repo.Name)
  continue
}
@bluekeyes
Copy link
Owner

What you have looks like a good start, but here are some suggestions to consider:

  • Make sure to handle file modes when creating files, including the fact that a patch can change modes. In general, prefer NewMode if it is set, then OldMode if it is set, and then default to 0o644 if neither is set.
  • Binary files should not need any special handling - calling gitdiff.Apply will do the right thing based on the patch type. A binary file can be created/deleted/copied/renamed, but you can handle this in the same case where you handle it for text files.
  • You can get pretty far by only handling new files, deleted files, and modified files. In practice, you probably won't see many copy/rename patches. Internally, Git stores these as deletions and additions and detects the rename or copy after the fact by comparing file content. You need to set non-default configuration or pass extra flags to git diff (or related commands) to generate a patch with the copy or rename headers.
  • It may seem strange at first, but you should still call gitdiff.Apply when deleting a file. This will make sure that the file you are deleting actually had the same contents as the patch that claims to delete it. If the file differs from the patch, Apply returns an error.
  • This is general Go thing, but be careful about calling defer file.Close() in a loop. These statements won't actually execute until the function that contains the loop returns, which means you can potentially accumulate a lot of open files.

If you do want to handle copies and renames:

  • Make sure to still call gitdiff.Apply! Copy and rename patches can also include file modifications because Git does the detection based on a similarity percentage, which is less than 100% by default.
  • In the case of empty patches (where there are no content changes), gitdiff.Apply copies the input content to the destination, so you shouldn't have to worry about calling os.Rename or manually copying content.
  • I'd probably handle copies and renames in the same branch, since the core logic is the same: open the old file, apply the patch to the old content, and write the result to the new file. The difference is in whether you keep the old file (copy) or delete it (rename). I think breaking these operations in to sequences of adds and deletes makes them easier to think about.

You may also find the implementation of patch2pr useful. This isn't quite the same, as it uses the GitHub API instead of local files, but does show how to use the library.

@gabrie30
Copy link
Author

gabrie30 commented Oct 1, 2024

Thank you so much for such a thoughtful reply!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants