9 Star 149 Fork 34

开源中国/git-repo-clean

加入 Gitee
与超过 1200万 开发者一起发现、参与优秀开源项目,私有仓库也完全免费 :)
免费加入
克隆/下载
repository.go 20.83 KB
一键复制 编辑 原始数据 按行查看 历史
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825
package main
import (
"bufio"
"bytes"
"errors"
"fmt"
"io"
"math"
"os"
"os/exec"
"sort"
"strconv"
"strings"
"path/filepath"
mapset "github.com/deckarep/golang-set"
)
var (
Branch_changed = mapset.NewSet() // record branches that has been changed
Files_changed = mapset.NewSet() // record files for LFS
Blob_size_list = make(map[string]string) // record repo's blob list
)
type Context struct {
workDir string
gitBin string
gitDir string
bare bool
opts *Options
scan_t ScanType
}
type ScanType struct {
filepath bool
filesize bool
filetype bool
}
type Repository struct {
context *Context
filtered []string
}
type HistoryRecord struct {
oid string
objectSize uint64
objectName string
}
type BlobList []HistoryRecord
func InitContext(path string) (*Context, error) {
// Find the `git` executable to be used:
gitBin, err := findGitBin()
if err != nil {
return nil, fmt.Errorf(LocalPrinter().Sprintf(
"couldn't find Git execute program: %s", err))
}
// check git version
version, err := GitVersion(gitBin)
if err != nil {
return nil, err
}
// Git version should >= 2.24.0
if GitVersionConvert(version) < 2240 {
return nil, fmt.Errorf(LocalPrinter().Sprintf(
"sorry, this tool requires Git version at least 2.24.0"))
}
// check is current repo is in bare repo
var bare bool
if b, err := IsBare(gitBin, path); b && err == nil {
bare = true
PrintLocalWithYellowln("bare repo warning")
}
// check if current repo has uncommited files
if !bare {
err = GetCurrentStatus(gitBin, path)
if err != nil {
PrintLocalWithRedln(LocalPrinter().Sprintf("%s", err))
os.Exit(1)
}
}
// check if current repo is in shallow repo
if shallow, err := IsShallow(gitBin, path); shallow {
return nil, err
}
gitdir, err := GitDir(gitBin, path)
if err != nil {
return nil, err
}
return &Context{
workDir: path, // worktree dir
gitDir: gitdir, // .git dir
gitBin: gitBin,
bare: bare,
opts: &op, // global
}, nil
}
func NewRepository() *Repository {
// init repo context
ctx, err := InitContext(op.path)
if err != nil {
PrintLocalWithRedln(LocalPrinter().Sprintf("%s", err))
os.Exit(1)
}
// important! get repo blob list
err = GetBlobSize(ctx.gitBin, ctx.workDir)
if err != nil {
ft := LocalPrinter().Sprintf("run getblobsize error: %s", err)
PrintRedln(ft)
}
// scan repo files
scanedfiles, err := ScanFiles(ctx)
if err != nil {
LocalFprintf(os.Stderr, "init repo filter error")
os.Exit(1)
}
return &Repository{
context: ctx,
filtered: scanedfiles,
}
}
func GetBlobName(gitbin, path, oid string) (string, error) {
cmd := exec.Command(gitbin, "-C", path, "rev-list", "--objects", "--all")
out, err := cmd.StdoutPipe()
if err != nil {
return "", err
}
cmd.Stderr = os.Stderr
err = cmd.Start()
if err != nil {
return "", err
}
blobname := ""
buf := bufio.NewReader(out)
for {
line, err := buf.ReadString('\n')
if err != nil {
if err == io.EOF {
break
}
return "", err
}
// drop LF
line = line[:len(line)-1]
if len(line) <= 41 {
continue
}
if line[0:40] == oid {
blobname = line[41:]
break
}
}
return blobname, nil
}
func parseBatchHeader(header string) (objectid, objecttype, objectsize string, err error) {
// drop LF
header = header[:len(header)-1]
infos := strings.Split(header, " ")
if infos[len(infos)-1] == "missing" {
return "", "", "", errors.New("got missing object")
}
return infos[0], infos[1], infos[2], nil
}
// GetBlobSize to get repository blobs list
func GetBlobSize(gitbin, path string) error {
cmd := exec.Command(gitbin, "-C", path, "cat-file", "--batch-all-objects",
"--batch-check=%(objectname) %(objecttype) %(objectsize)")
out, err := cmd.StdoutPipe()
if err != nil {
return err
}
cmd.Stderr = os.Stderr
err = cmd.Start()
if err != nil {
return err
}
buf := bufio.NewReader(out)
for {
line, err := buf.ReadString('\n')
if err != nil {
if err == io.EOF {
break
}
return err
}
objectid, objecttype, objectsize, err := parseBatchHeader(line)
if err != nil {
return err
}
if objecttype == "blob" {
Blob_size_list[objectid] = objectsize
}
}
return nil
}
func ScanRepository(context *Context) (BlobList, error) {
var empty BlobList
var blobs BlobList
if context.opts.verbose {
PrintLocalWithGreenln("start scanning")
}
for objectid, objectsize := range Blob_size_list {
// set bitsize to 64, means max single blob size is 4 GiB
actual_size, _ := strconv.ParseUint(objectsize, 10, 64)
// e.g. git repo-clean -s -l=1000k --lfs --type=po --delete
if context.opts.lfs && !context.opts.interact {
limit, err := UnitConvert(context.opts.limit)
if err != nil {
return empty, fmt.Errorf(LocalPrinter().Sprintf(
"convert uint error: %s", err))
}
// to protect LFS file itself
if limit < 200 {
context.opts.limit = LFS_SAFE_SIZE
}
if actual_size > limit {
name, err := GetBlobName(context.gitBin, context.workDir, objectid)
if err != nil && err != io.EOF {
return empty, fmt.Errorf(LocalPrinter().Sprintf(
"run GetBlobName error: %s", err))
}
if name == "" {
continue
}
if len(context.opts.types) != 0 && context.opts.types != DefaultFileType {
extent := filepath.Ext(name)
if extent == "."+context.opts.types {
// append this record blob into slice
blobs = append(blobs, HistoryRecord{objectid, actual_size, name})
// sort according by size
sort.Slice(blobs, func(i, j int) bool {
return blobs[i].objectSize > blobs[j].objectSize
})
}
}
}
} else {
limit, err := UnitConvert(context.opts.limit)
if err != nil {
return empty, fmt.Errorf(LocalPrinter().Sprintf(
"convert uint error: %s", err))
}
if actual_size > limit {
name, err := GetBlobName(context.gitBin, context.workDir, objectid)
if err != nil && err != io.EOF {
return empty, fmt.Errorf(LocalPrinter().Sprintf(
"run GetBlobName error: %s", err))
}
if name == "" {
continue
}
if len(context.opts.types) != 0 && context.opts.types != DefaultFileType {
extent := filepath.Ext(name)
if extent != "."+context.opts.types {
// matched none, skip
continue
}
}
// append this record blob into slice
blobs = append(blobs, HistoryRecord{objectid, actual_size, name})
// sort according by size
sort.Slice(blobs, func(i, j int) bool {
return blobs[i].objectSize > blobs[j].objectSize
})
// remain first {op.number} blobs
if len(blobs) > int(context.opts.number) {
blobs = blobs[:context.opts.number]
// break
}
}
}
}
return blobs, nil
}
func ScanMode(ctx *Context) (result []string) {
var first_target []string
bloblist, err := ScanRepository(ctx)
if err != nil {
ft := LocalPrinter().Sprintf("scanning repository error: %s", err)
PrintRedln(ft)
os.Exit(1)
}
if len(bloblist) == 0 {
PrintLocalWithRedln("no files were scanned")
os.Exit(1)
} else {
ShowScanResult(bloblist)
}
if ctx.opts.interact {
first_target = MultiSelectCmd(bloblist)
if len(bloblist) != 0 && len(first_target) == 0 {
PrintLocalWithRedln("no files were selected")
os.Exit(1)
}
var ok = false
ok, result = Confirm(first_target)
if !ok {
PrintLocalWithRedln("operation aborted")
os.Exit(1)
}
} else {
for _, item := range bloblist {
result = append(result, item.oid)
}
}
// record target file's name
for _, item := range bloblist {
for _, target := range result {
if item.oid == target {
Files_changed.Add(item.objectName)
}
}
}
return result
}
func NonScanMode(ctx *Context, file_limit string, file_type string, file_num uint32) {
ctx.opts.limit = file_limit
ctx.opts.types = file_type
ctx.opts.number = file_num
}
func ScanFiles(ctx *Context) ([]string, error) {
var scanned_targets []string
// when run git-repo-clean -i, its means run scan too
if ctx.opts.interact {
ctx.opts.scan = true
ctx.opts.delete = true
ctx.opts.verbose = true
ctx.opts.lfs = true
if err := ctx.opts.SurveyCmd(); err != nil {
ft := LocalPrinter().Sprintf("ask question module fail: %s", err)
PrintRedln(ft)
os.Exit(1)
}
}
// set default branch to all is to keep deleting process consistent with scanning process
// user end pass '--branch=all', but git-fast-export takes '--all'
if op.branch == DefaultRepoBranch {
op.branch = "--all"
}
if ctx.opts.lfs {
limit, _ := UnitConvert(ctx.opts.limit)
if limit < 200 {
ctx.opts.limit = LFS_SAFE_SIZE
}
// can't run lfs-migrate in bare repo
// git lfs track must be run in a work tree.
if ctx.bare {
PrintLocalWithYellowln("bare repo error")
os.Exit(1)
}
}
if ctx.opts.limit == DefaultFileSize && ctx.opts.scan {
ctx.opts.limit = "1M" // set default to 1M for scan
}
PrintLocalWithPlain("current repository size")
PrintLocalWithYellowln(GetDatabaseSize(ctx.workDir, ctx.bare))
if lfs := GetLFSObjSize(ctx.workDir); len(lfs) > 0 {
PrintLocalWithPlain("including LFS objects size")
PrintLocalWithYellowln(lfs)
}
if ctx.opts.scan {
scanned_targets = ScanMode(ctx)
} else if ctx.opts.files != nil {
/* Filter by provided files
* Default: file size limit and file type
* Max file number limit
*/
ctx.scan_t.filepath = true
NonScanMode(ctx, DefaultFileSize, DefaultFileType, math.MaxUint32)
} else if ctx.opts.limit != DefaultFileSize {
/* Filter by file size
* Default: file type
* Max file number limit
*/
ctx.scan_t.filesize = true
NonScanMode(ctx, ctx.opts.limit, DefaultFileType, math.MaxUint32)
} else if ctx.opts.types != DefaultFileType {
/* Filter by file type
* Default: file size limit
* Max file number limit
*/
ctx.scan_t.filetype = true
NonScanMode(ctx, DefaultFileSize, ctx.opts.types, math.MaxUint32)
}
if !ctx.opts.delete {
os.Exit(1)
}
if (ctx.scan_t.filepath || ctx.scan_t.filesize || ctx.scan_t.filetype) && ctx.opts.lfs {
PrintLocalWithRedln("Convert LFS file error")
os.Exit(1)
}
return scanned_targets, nil
}
// GitDir get .git dir in repository with absolute path
func GitDir(gitbin, path string) (string, error) {
cmd := exec.Command(gitbin, "-C", path, "rev-parse", "--absolute-git-dir")
out, err := cmd.Output()
if err != nil {
return "", fmt.Errorf(
"could not run 'git rev-parse --git-dir': %s", err,
)
}
return string(bytes.TrimSpace(out)), nil
}
// check if the current repository is bare repo
func IsBare(gitbin, path string) (bool, error) {
cmd := exec.Command(gitbin, "-C", path, "rev-parse", "--is-bare-repository")
out, err := cmd.Output()
if err != nil {
return false, fmt.Errorf(LocalPrinter().Sprintf(
"could not run 'git rev-parse --is-bare-repository': %s", err),
)
}
return string(bytes.TrimSpace(out)) == "true", nil
}
// check if the current repository is shallow repo, need Git version 2.15.0 or newer
func IsShallow(gitbin, path string) (bool, error) {
cmd := exec.Command(gitbin, "-C", path, "rev-parse", "--is-shallow-repository")
out, err := cmd.Output()
if err != nil {
return false, fmt.Errorf(LocalPrinter().Sprintf(
"could not run 'git rev-parse --is-shallow-repository': %s", err),
)
}
if string(bytes.TrimSpace(out)) == "true" {
return true, fmt.Errorf(LocalPrinter().Sprintf("could not run in a shallow repo"))
}
return false, nil
}
// check if the current repository is flesh clone.
func IsFresh(gitbin, path string) (bool, error) {
cmd := exec.Command(gitbin, "-C", path, "reflog", "show")
out, err := cmd.Output()
if err != nil {
return false, fmt.Errorf(LocalPrinter().Sprintf(
"could not run 'git reflog show': %s", err),
)
}
return strings.Count(string(out), "\n") < 2, nil
}
// check if Git-LFS has installed in host machine
func HasLFSEnv(gitbin string) (bool, error) {
cmd := exec.Command(gitbin, "lfs", "version")
out, err := cmd.Output()
if err != nil {
return false, fmt.Errorf(LocalPrinter().Sprintf("could not run 'git lfs version': %s", err))
}
// #FIXME $?
return strings.Contains(string(out), "git-lfs"), nil
}
// get git version string
func GitVersion(gitbin string) (string, error) {
cmd := exec.Command(gitbin, "version")
out, err := cmd.Output()
if err != nil {
return "", fmt.Errorf(LocalPrinter().Sprintf("could not run 'git version': %s", err))
}
matches := Match("[0-9]+.[0-9]+.[0-9]+.?[0-9]?", string(out))
if len(matches) == 0 {
return "", fmt.Errorf(LocalPrinter().Sprintf("match git version wrong"))
}
return matches[0], nil
}
// convert version string to int number for compare. e.g. convert 2.33.0 to 2330
func GitVersionConvert(version string) int {
var vstr string
dict := strings.Split(version, ".")
if len(dict) == 3 {
vstr = dict[0] + dict[1] + dict[2]
}
if len(dict) == 4 {
vstr = dict[0] + dict[1] + dict[2] + dict[3]
}
vstr = strings.TrimSpace(vstr)
ret, err := strconv.Atoi(vstr)
if err != nil {
return 0
}
return ret
}
// get current branch
func GetCurrentBranch(gitbin, path string) (string, error) {
cmd := exec.Command(gitbin, "-C", path, "symbolic-ref", "HEAD", "--short")
out, err := cmd.Output()
if err != nil {
return "", fmt.Errorf(LocalPrinter().Sprintf("could not run 'git symbolic-ref HEAD --short': %s", err))
}
return strings.TrimSuffix(string(out), "\n"), nil
}
// get current status
func GetCurrentStatus(gitbin, path string) error {
cmd := exec.Command(gitbin, "-C", path, "status", "-s")
out, err := cmd.Output()
if err != nil {
return fmt.Errorf(LocalPrinter().Sprintf("could not run 'git status'"))
}
st := string(out)
if st == "" {
return nil
}
list := strings.Split(st, "\n")
for _, ele := range list {
if strings.HasPrefix(ele, "M ") || strings.HasPrefix(ele, " M") || strings.HasPrefix(ele, "A ") {
return fmt.Errorf(LocalPrinter().Sprintf("there's some changes to be committed, please commit them first"))
}
}
return nil
}
// get git objects data size
func GetDatabaseSize(dir string, bare bool) string {
var path string
if bare {
path = filepath.Join(dir, ".")
} else {
path = filepath.Join(dir, ".git/objects")
}
cmd := exec.Command("du", "-hs", path)
out, err := cmd.Output()
if err != nil {
PrintLocalWithRedln("could not run 'du -hs'")
}
return strings.TrimSuffix(string(out), "\n")
}
// get lfs objects data size
func GetLFSObjSize(dir string) string {
path := filepath.Join(dir, ".git/lfs")
if _, err := os.Stat(path); err == nil {
cmd := exec.Command("du", "-hs", path)
out, err := cmd.Output()
if err != nil {
PrintLocalWithRedln("could not run 'du -hs .git/lfs/'")
}
return strings.TrimSuffix(string(out), "\n")
}
return ""
}
// get repo GC url if the repo is hosted on Gitee.com
func GetGiteeGCWeb(gitbin, path string) string {
cmd := exec.Command(gitbin, "-C", path, "config", "--get", "remote.origin.url")
out, err := cmd.Output()
if err != nil {
return ""
}
url := string(out)
if url == "" {
return ""
}
if strings.Contains(url, "gitee.com") {
if strings.HasPrefix(url, "git@") {
url = strings.TrimPrefix(url, "git@")
url = "https://" + strings.Replace(url, ":", "/", 1)
}
url = strings.TrimSuffix(url, ".git\n") + "/settings#git-gc"
} else {
return ""
}
return url
}
func GetRepoPath(gitbin, path string) string {
cmd := exec.Command(gitbin, "-C", path, "worktree", "list")
out, err := cmd.Output()
if err != nil {
return ""
}
repopath := strings.Split(string(out), " ")[0]
return repopath
}
func BackUp(gitbin, path string) {
repo_path := GetRepoPath(gitbin, path)
dst := repo_path + ".bak"
// check if the same directory exist
_, err := os.Stat(dst)
if err == nil {
ok := AskForOverride()
if !ok {
PrintLocalWithYellowln("backup canceled")
return
} else {
os.RemoveAll(dst)
}
}
PrintLocalWithGreenln("start backup")
cmd := exec.Command(gitbin, "-C", path, "clone", "--no-local", path, dst)
_, err = cmd.Output()
if err != nil {
PrintLocalWithRedln("clone error")
return
}
abs_path, err := filepath.Abs(dst)
if err != nil {
PrintLocalWithRedln("run filepach.Abs error")
}
ft := LocalPrinter().Sprintf("backup done! Backup file path is: %s", abs_path)
PrintYellowln(ft)
}
func PushRepo(gitbin, path string) error {
cmd := exec.Command(gitbin, "-C", path, "push", "origin", "--all", "--force", "--porcelain")
out1, err := cmd.Output()
if err != nil {
PrintLocalWithRedln("Push failed")
return err
}
PrintYellowln(strings.TrimSuffix(string(out1), "\n"))
cmd2 := exec.Command(gitbin, "-C", path, "push", "origin", "--tags", "--force")
out2, err := cmd2.Output()
if err != nil {
PrintLocalWithRedln("Push failed")
return err
}
PrintYellowln(strings.TrimSuffix(string(out2), "\n"))
PrintLocalWithYellowln("Done")
return nil
}
// BrachesChanged prints all branches that have been changed
func BrachesChanged() bool {
branches := Branch_changed.ToSlice()
if len(branches) != 0 {
PrintLocalWithYellowln("branches have been changed")
for _, branch := range branches {
s := strings.TrimSpace(branch.(string))
if strings.HasPrefix(s, "refs/heads/") {
PrintYellowln(strings.TrimPrefix(s, "refs/heads/"))
}
if strings.HasPrefix(s, "refs/tags/") {
PrintYellowln(strings.TrimPrefix(s, "refs/tags/"))
}
if strings.HasPrefix(s, "refs/remotes/") {
PrintYellowln(strings.TrimPrefix(s, "refs/remotes/"))
}
}
fmt.Println()
return true
}
return false
}
// FilesChanged prints all files that have been changed
func FilesChanged() {
files := Files_changed.ToSlice()
if len(files) != 0 {
PrintLocalWithPlainln("file have been changed")
for _, file := range files {
PrintYellowln(file.(string))
}
}
}
func (context Context) CleanUp() {
if BrachesChanged() || context.opts.lfs {
// clean up
PrintLocalWithGreenln("file cleanup is complete. Start cleaning the repository")
} else {
// exit
PrintLocalWithYellowln("nothing have changed, exit...")
os.Exit(1)
}
if !context.bare {
fmt.Println("running git reset --hard --quiet")
cmd1 := exec.Command(context.gitBin, "-C", context.workDir, "reset", "--hard", "--quiet")
cmd1.Stdout = os.Stdout
err := cmd1.Start()
if err != nil {
PrintRedln(fmt.Sprint(err))
}
err = cmd1.Wait()
if err != nil {
PrintRedln(fmt.Sprint(err))
}
}
fmt.Println("running git reflog expire --expire=now --all")
cmd2 := exec.Command(context.gitBin, "-C", context.workDir, "reflog", "expire", "--expire=now", "--all")
cmd2.Stderr = os.Stderr
cmd2.Stdout = os.Stdout
err := cmd2.Start()
if err != nil {
PrintRedln(fmt.Sprint(err))
}
err = cmd2.Wait()
if err != nil {
PrintRedln(fmt.Sprint(err))
}
fmt.Println("running git gc --prune=now --quiet")
cmd3 := exec.Command(context.gitBin, "-C", context.workDir, "gc", "--prune=now", "--quiet")
cmd3.Stderr = os.Stderr
cmd3.Stdout = os.Stdout
err = cmd3.Start()
if err != nil {
PrintRedln(fmt.Sprint(err))
}
cmd3.Wait()
if err != nil {
PrintRedln(fmt.Sprint(err))
}
fmt.Println("running git gc --aggressive --quiet")
cmd4 := exec.Command(context.gitBin, "-C", context.workDir, "gc", "--aggressive", "--quiet")
cmd4.Stderr = os.Stderr
cmd4.Stdout = os.Stdout
err = cmd4.Start()
if err != nil {
PrintRedln(fmt.Sprint(err))
}
cmd4.Wait()
if err != nil {
PrintRedln(fmt.Sprint(err))
}
}
func LFSPrompt() {
FilesChanged()
PrintLocalWithPlainln("before you push to remote, you have to do something below:")
PrintLocalWithYellowln("1. install git-lfs")
PrintLocalWithYellowln("2. run command: git lfs install")
PrintLocalWithYellowln("3. edit .gitattributes file")
PrintLocalWithYellowln("4. commit your .gitattributes file.")
}
func (context Context) Prompt() {
PrintLocalWithGreenln("cleaning completed")
PrintLocalWithPlain("current repository size")
PrintLocalWithYellowln(GetDatabaseSize(context.workDir, context.bare))
if lfs := GetLFSObjSize(context.workDir); len(lfs) > 0 {
PrintLocalWithPlain("including LFS objects size")
PrintLocalWithYellowln(lfs)
}
if context.opts.lfs {
LFSPrompt()
}
var pushed bool
if !context.opts.lfs {
if AskForUpdate() {
PrintLocalWithPlainln("execute force push")
PrintLocalWithYellowln("git push origin --all --force")
PrintLocalWithYellowln("git push origin --tags --force")
err := PushRepo(context.gitBin, context.workDir)
if err == nil {
pushed = true
}
}
}
PrintLocalWithPlainln("suggest operations header")
if pushed {
PrintLocalWithGreenln("1. (Done!)")
fmt.Println()
} else {
PrintLocalWithRedln("1. (Undo)")
PrintLocalWithRedln(" git push origin --all --force")
PrintLocalWithRedln(" git push origin --tags --force")
fmt.Println()
}
PrintLocalWithRedln("2. (Undo)")
url := GetGiteeGCWeb(context.gitBin, context.workDir)
if url != "" {
PrintLocalWithRed("gitee GC page link")
PrintYellowln(url)
}
fmt.Println()
PrintLocalWithRedln("3. (Undo)")
PrintLocalWithRed("for detailed documentation, see")
PrintYellowln("https://gitee.com/oschina/git-repo-clean/blob/main/docs/repo-update.md")
fmt.Println()
if !context.opts.interact {
PrintLocalWithPlainln("introduce GIT LFS")
PrintLocalWithPlain("for the use of Gitee LFS, see")
PrintYellowln("https://gitee.com/help/articles/4235")
}
}
马建仓 AI 助手
尝试更多
代码解读
代码找茬
代码优化
Go
1
https://gitee.com/oschina/git-repo-clean.git
git@gitee.com:oschina/git-repo-clean.git
oschina
git-repo-clean
git-repo-clean
main

搜索帮助