R语言 如何获取给定对象定义的git历史?

zbsbpyhn  于 2023-09-27  发布在  Git
关注(0)|答案(1)|浏览(89)

在处理一个包时,我经常想知道这个函数最近是否被修改过。
函数可能已从一个脚本移动到另一个脚本,其代码或文档可能已更改。
如果能打电话就太好了:git_history(my_function, branch = "main"),并获得例如 Dataframe ,其中:

  • commit_id
  • 提交文本
  • 作者
  • 时间戳
  • 文件
  • 代码(对象定义,理想情况下但不一定包括roxygen 2块)

通过使用{diffobj}包,我们将能够轻松地导航更改。
如果它能处理所有对象就好了,但如果只处理使用srcref的函数就更容易了,这样我们就能得到95%的值。
更好的方法是增加一个包含对象名称的列,并将所有内容放在一个df中,但这样做可能会有点昂贵?
我有一些关于如何实现这一点的想法,但我不确定这是否有效,如果已经完成了,我不想在这上面花时间。如果我没有得到满意的答案,我会回到这个问题上来回答自己。
我看到的方式是:

  • 使用git命令获取提交列表
  • 循环它们,并将它们签入临时文件夹
  • 在每次迭代中,解析getParseData(parse(file = ))所有文件并提取定义,或者只是正则化它们,或者源所有文件并提取srcref(这不会给予我们roxygen 2的东西,也不会给我们非函数定义)
  • 为每一个提交(以及每一个对象定义,如果我们这样做的话)构建一行上述df
  • 只为连续重复代码保留最旧的
  • 删除临时文件夹,返回数据框

我认为如果有一个模板来指导如何使用R和git进行一些语言感知的版本控制,那就太好了,所以让我们在这里做吧。

bxgwgixi

bxgwgixi1#

我提供了一个函数来获取给定分支的存储库中所有函数的历史,以及一个函数来使用获取的历史查看给定对象的历史。
前一个函数在大型存储库中可能会花费一些时间,但我已经放了一些进度条来帮助。
一些参数来限制文件范围和期间也会有帮助。这应该在一个包里。

git_history <- function(repo = ".", branch = "main") {
  #-----------------------------------------------------------------------------
  # get commit history + meta data
  log_cmd <- sprintf("git -C '%s' log %s --pretty=format:'%%h,%%ae,%%cI,%%s'", repo, branch)
  commits <- system(log_cmd, intern = TRUE)
  
  #-----------------------------------------------------------------------------
  # wrangle into data.frame, taking into account that commas in commit title are not separators
  commits_df <- purrr::map_dfr(
    strsplit(commits, ","),
    ~ tibble::tibble(
      commit = .x[[1]],
      author = .x[[2]],
      time = .x[[3]] |> sub("T", "", x = _) |> sub("+", " (+", x = _) |> paste0(")"),
      description = paste(.x[-(1:3)], collapse = ","),
    ), 
    .progress = TRUE
  )
  
  #-----------------------------------------------------------------------------
  # Fetch affected files for every commit
  get_affected_files <- function(commit) {
    cmd <- sprintf("git -C '%s' show --name-only --pretty=format: %s", repo, commit)
    files <- system(cmd, intern = TRUE)
    # keep only R files from the R folder
    files <- grep("R/.*[.](R|r)$", files, value = TRUE)
    files
  }
  
  files_df <-
    commits_df |>
    dplyr::mutate(file = purrr::map(commit, get_affected_files, .progress = TRUE)) |>
    tidyr::unnest_longer(file)
  
  #-----------------------------------------------------------------------------
  # Fetch objects for every affected file
  parse_commit_file  <- function(commit, file) {
    cmd <- sprintf("git -C '%s' show %s:'%s'", repo, commit, file)
    file_code <- suppressWarnings(tryCatch(
      system(cmd, intern = TRUE, ignore.stderr = TRUE), 
      error = function(e) "REMOVED"
    ))
    parsed_data <- try(getParseData(parse(text = file_code, keep.source = TRUE), includeText = TRUE), silent = TRUE)
    # non syntactic edits (e.g. containing conflict markers) are skipped
    if (inherits(parsed_data, "try-error")) return(NULL) 
    assignment_ids <- parsed_data$parent[parsed_data$token == "LEFT_ASSIGN"]
    top_level_assignment_ids <- parsed_data$id[parsed_data$id %in% assignment_ids & parsed_data$parent == 0]
    if (!length(top_level_assignment_ids)) return(NULL)
    obj_names <- sapply(top_level_assignment_ids, function(x) parsed_data$text[which(parsed_data$parent == x)[[1]]])
    parsed_data$id <- ifelse(parsed_data$parent < 0, abs(parsed_data$parent), parsed_data$id)
    parsed_data <- parsed_data[parsed_data$id %in% top_level_assignment_ids, c("id", "text")]
    code <- tapply(parsed_data$text, parsed_data$id, function(x) paste(x, collapse = "\n"))
    file_data <- data.frame(object = obj_names, code = code)
    file_data
  }
  
  rleid <- function(x) {
    x <- rle(x)$lengths
    rep(seq_along(x), times=x)
  }
  
  
  objects_df <-
    files_df |>
    dplyr::mutate(object = purrr::pmap(list(commit, file), parse_commit_file, .progress = TRUE)) |>
    tidyr::unnest(object)
  
  #-----------------------------------------------------------------------------
  # Remove redundancies and format into proper history df
  history <-
    objects_df |>
    # add NA code and file for every combination of commit and object
    tidyr::complete(tidyr::nesting(commit, author, time, description), object) |>
    # arrange chronologically to prepare for rleid() below
    dplyr::arrange(time) |> 
    dplyr::group_by(object) |> 
    # fill these NAs to reflect actual status at every step
    tidyr::fill(code, file) |>
    # identify running length sequences (spans in which object didn't change)
    dplyr::mutate(id = rleid(paste(file, code))) |>
    dplyr::ungroup() |>
    # keep only 1st and last during unchanged span
    dplyr::slice(
      .by = c(object, id), 
      unique(c(1, dplyr::n()))
    ) |>
    # replace file and code with intuitive values for diffobj in review_object_history()
    tidyr::replace_na(list(file = "none", code = "")) |>
    dplyr::select(-id)
  
  history
}

review_object_history <- function(git_history, object, ascending = TRUE) {
  diff_data <- git_history |> 
    dplyr::arrange(time) |>
    dplyr::filter(object == .env$object) |> 
    dplyr::transmute(
      banner = sprintf("%s<br>%s<br>%s<br>%s<br>%s", time, commit, author, description, file),
      file,
      code = strsplit(code, "\n")
    )
  
  if (ascending) {
    i <- 0
  } else {
    i <- nrow(diff_data)
  }
  tmp <- tempfile(fileext = ".html")
  press <- ""
  repeat {
    step <- prod(ifelse(c(press == "", ascending), 1, -1))
    i <- i + step
    if (i %in% c(0, nrow(diff_data))) break
    break_ <- FALSE
    while(identical(diff_data$code[[i+1]], diff_data$code[[i]]) &&
          identical(diff_data$file[[i+1]], diff_data$file[[i]])) {
      i <- i + step
      if (i %in% c(0, nrow(diff_data))) {
        break_ <- TRUE
        break
      }
    }
    if (break_) break
    
    x <- diffobj::diffChr(
      diff_data$code[[i]], 
      diff_data$code[[i+1]], 
      mode = "sidebyside", 
      tar.banner = paste("older:", diff_data$banner[[i]]),
      cur.banner = paste("newer:", diff_data$banner[[i+1]]),
      ignore.white.space = FALSE,
      convert.hz.white.space = FALSE,
      strip.sgr = FALSE,
      trim = FALSE,
      context = -1,
      pager = list(file.path = tmp)
    )
    print(x)
    press <- readline("Press ENTER to continue, or any other key then ENTER to go back: ")
  }
}

hist_data <- git_history() 
review_object_history(hist_data, "myobject")

相关问题