java 使用JGit操作 git - 工具类
注意先引入依赖包。
<dependency> <groupId>org.eclipse.jgit</groupId> <artifactId>org.eclipse.jgit</artifactId> <version>5.13.0.202109080827-r</version> </dependency>
工具类:
@Slf4j public class GitTools implements InitializingBean { protected Log logger = LogFactory.getLog(getClass()); private String uri = "https://gitlab.xxxxx.git"; private File basedir; public String getUri() { return uri; } /** * Username for authentication with remote repository. */ private String username; /** * Password for authentication with remote repository. */ private String password; /** * Error message for URI for git repo. */ public static final String MESSAGE = "You need to configure a uri for the git repository."; private static final String FILE_URI_PREFIX = "file:"; private static final String LOCAL_BRANCH_REF_PREFIX = "refs/remotes/origin/"; /** * Timeout (in seconds) for obtaining HTTP or SSH connection (if applicable). Default 5 seconds. */ private int timeout; /** * Time (in seconds) between refresh of the git repository. */ private int refreshRate = 0; /** * Time of the last refresh of the git repository. */ private long lastRefresh; /** * Flag to indicate that the repository should be cloned on startup (not on demand). Generally leads to slower * startup but faster first query. */ private boolean cloneOnStart; private JGitEnvironmentRepository.JGitFactory gitFactory; private String defaultLabel; /** * Factory used to create the credentials provider to use to connect to the Git repository. */ private GitCredentialsProviderFactory gitCredentialsProviderFactory = new GitCredentialsProviderFactory(); /** * Transport configuration callback for JGit commands. */ private TransportConfigCallback transportConfigCallback; /** * Flag to indicate that the repository should force pull. If true discard any local changes and take from remote * repository. */ private boolean forcePull; private boolean initialized; /** * Flag to indicate that the branch should be deleted locally if it's origin tracked branch was removed. */ private boolean deleteUntrackedBranches; /** * Flag to indicate that SSL certificate validation should be bypassed when communicating with a repository served * over an HTTPS connection. */ private boolean skipSslValidation; public GitTools(String basedir, String username, String password, boolean skipSslValidation) { this.username = username; this.password = password; this.cloneOnStart = true; this.defaultLabel = "master"; this.forcePull = true; this.timeout = 2000; this.deleteUntrackedBranches = true; this.refreshRate = 5; // 5s刷新一次 this.skipSslValidation = skipSslValidation; //是否跳过ssl this.gitFactory = new JGitEnvironmentRepository.JGitFactory(true); if (!StringUtils.hasText(basedir)) { this.basedir = createBaseDir(); } else { this.basedir = new File(basedir); } logger.info("basedir => " + basedir); } public void setTransportConfigCallback(TransportConfigCallback transportConfigCallback) { this.transportConfigCallback = transportConfigCallback; } public JGitEnvironmentRepository.JGitFactory getGitFactory() { return this.gitFactory; } public void setGitFactory(JGitEnvironmentRepository.JGitFactory gitFactory) { this.gitFactory = gitFactory; } public void setGitCredentialsProviderFactory(GitCredentialsProviderFactory gitCredentialsProviderFactory) { this.gitCredentialsProviderFactory = gitCredentialsProviderFactory; } GitCredentialsProviderFactory getGitCredentialsProviderFactory() { return gitCredentialsProviderFactory; } public String getDefaultLabel() { return this.defaultLabel; } public void setDefaultLabel(String defaultLabel) { this.defaultLabel = defaultLabel; } public boolean isForcePull() { return this.forcePull; } public void setForcePull(boolean forcePull) { this.forcePull = forcePull; } public boolean isDeleteUntrackedBranches() { return this.deleteUntrackedBranches; } public void setDeleteUntrackedBranches(boolean deleteUntrackedBranches) { this.deleteUntrackedBranches = deleteUntrackedBranches; } public boolean isSkipSslValidation() { return this.skipSslValidation; } public void setSkipSslValidation(boolean skipSslValidation) { this.skipSslValidation = skipSslValidation; } @Override public synchronized void afterPropertiesSet() throws Exception { Assert.state(getUri() != null, MESSAGE); initialize(); if (this.cloneOnStart) { initClonedRepository(); } } public static final String SEPARATOR = "/"; public String getBranchRealName(String ref) { return ref.substring(ref.lastIndexOf(SEPARATOR) + 1); } public void refreshAllBranch() throws Exception { afterPropertiesSet(); Git git = null; try { git = createGitClient(); List<Ref> refs = git.branchList().setListMode(ListBranchCommand.ListMode.REMOTE).call(); for (Ref ref : refs) { getFilesFromRemote(git, getBranchRealName(ref.getName())); } } catch (RefNotFoundException e) { throw new NoSuchLabelException("No such label: " + e); } catch (NoRemoteRepositoryException e) { throw new NoSuchRepositoryException("No such repository: " + getUri(), e); } catch (GitAPIException e) { throw new NoSuchRepositoryException("Cannot clone or checkout repository: " + getUri(), e); } catch (Exception e) { throw new IllegalStateException("Cannot load environment", e); } finally { try { if (git != null) { git.close(); } } catch (Exception e) { this.logger.warn("Could not close git repository", e); } } } public void getFilesFromRemote(Git git, String label) throws GitAPIException, IOException { // checkout after fetch so we can get any new branches, tags, ect. // if nothing to update so just checkout and merge. // Merge because remote branch could have been updated before FetchResult fetchStatus = fetch(git, label); if (this.deleteUntrackedBranches && fetchStatus != null) { deleteUntrackedLocalBranches(fetchStatus.getTrackingRefUpdates(), git); } checkout(git, label); tryMerge(git, label); System.out.println("============= branch now =>" + label); readFile(new File(git.getRepository().getWorkTree().getAbsolutePath() + "/env")); } public void readFile(File file) throws IOException { if (file.isDirectory()) { for (final File listFile : file.listFiles()) { if (listFile.isDirectory()) { readFile(listFile); } else { showFileContent(listFile); } } } else { showFileContent(file); } } public void showFileContent(File file) throws IOException { if (file.isFile()) { System.out.println(System.lineSeparator() + "开始读取文件: " + file.getName()); RandomAccessFile randomAccessFile = new RandomAccessFile(file, "r"); String str; while ((str = randomAccessFile.readLine()) != null) { System.out.println(str); } randomAccessFile.close(); } } /** * Get the working directory ready. * * @param label label to refresh * @return head id */ public String refresh(String label) { Git git = null; try { git = createGitClient(); if (shouldPull(git)) { FetchResult fetchStatus = fetch(git, label); if (this.deleteUntrackedBranches && fetchStatus != null) { deleteUntrackedLocalBranches(fetchStatus.getTrackingRefUpdates(), git); } } // checkout after fetch so we can get any new branches, tags, ect. // if nothing to update so just checkout and merge. // Merge because remote branch could have been updated before checkout(git, label); tryMerge(git, label); // always return what is currently HEAD as the version return git.getRepository().findRef("HEAD").getObjectId().getName(); } catch (RefNotFoundException e) { throw new NoSuchLabelException("No such label: " + label, e); } catch (NoRemoteRepositoryException e) { throw new NoSuchRepositoryException("No such repository: " + getUri(), e); } catch (GitAPIException e) { throw new NoSuchRepositoryException("Cannot clone or checkout repository: " + getUri(), e); } catch (Exception e) { throw new IllegalStateException("Cannot load environment", e); } finally { try { if (git != null) { git.close(); } } catch (Exception e) { this.logger.warn("Could not close git repository", e); } } } private void tryMerge(Git git, String label) { try { if (isBranch(git, label)) { // merge results from fetch merge(git, label); if (!isClean(git, label)) { this.logger.warn("The local repository is dirty or ahead of origin. Resetting" + " it to origin/" + label + "."); // resetHard(git, label, LOCAL_BRANCH_REF_PREFIX + label); } } } catch (GitAPIException e) { throw new NoSuchRepositoryException("Cannot clone or checkout repository: " + getUri(), e); } } /** * Clones the remote repository and then opens a connection to it. Checks out to the defaultLabel if specified. * * @throws GitAPIException when cloning fails * @throws IOException when repo opening fails */ private void initClonedRepository() throws GitAPIException, IOException { deleteBaseDirIfExists(); Git git = cloneToBasedir(); if (git != null) { git.close(); } git = openGitRepository(); // Check if git points to valid repository and default label is not empty or // null. if (null != git && git.getRepository() != null && !StringUtils.isEmpty(getDefaultLabel())) { // Checkout the default branch set for repo in git. This may not always be // master. It depends on the // admin and organization settings. String defaultBranchInGit = git.getRepository().getBranch(); // If default branch is not empty and NOT equal to defaultLabel, then // checkout the branch/tag/commit-id. if (!StringUtils.isEmpty(defaultBranchInGit) && !getDefaultLabel().equalsIgnoreCase(defaultBranchInGit)) { checkout(git, getDefaultLabel()); } } if (git != null) { git.close(); } } /** * Deletes local branches if corresponding remote branch was removed. * * @param trackingRefUpdates list of tracking ref updates * @param git git instance * @return list of deleted branches */ private Collection<String> deleteUntrackedLocalBranches(Collection<TrackingRefUpdate> trackingRefUpdates, Git git) { if (CollectionUtils.isEmpty(trackingRefUpdates)) { return Collections.emptyList(); } Collection<String> branchesToDelete = new ArrayList<>(); for (TrackingRefUpdate trackingRefUpdate : trackingRefUpdates) { ReceiveCommand receiveCommand = trackingRefUpdate.asReceiveCommand(); if (receiveCommand.getType() == DELETE) { String localRefName = trackingRefUpdate.getLocalName(); if (StringUtils.startsWithIgnoreCase(localRefName, LOCAL_BRANCH_REF_PREFIX)) { String localBranchName = localRefName.substring( LOCAL_BRANCH_REF_PREFIX.length(), localRefName.length() ); branchesToDelete.add(localBranchName); } } } if (CollectionUtils.isEmpty(branchesToDelete)) { return Collections.emptyList(); } try { // make sure that deleted branch not a current one checkout(git, this.defaultLabel); return deleteBranches(git, branchesToDelete); } catch (Exception ex) { String message = String.format("Failed to delete %s branches.", branchesToDelete); warn(message, ex); return Collections.emptyList(); } } private List<String> deleteBranches(Git git, Collection<String> branchesToDelete) throws GitAPIException { DeleteBranchCommand deleteBranchCommand = git.branchDelete() .setBranchNames(branchesToDelete.toArray(new String[0])) // local branch can contain data which is not merged to HEAD - force // delete it anyway, since local copy should be R/O .setForce(true); List<String> resultList = deleteBranchCommand.call(); this.logger.info( String.format("Deleted %s branches from %s branches to delete.", resultList, branchesToDelete)); return resultList; } private Ref checkout(Git git, String label) throws GitAPIException { CheckoutCommand checkout = git.checkout(); if (shouldTrack(git, label)) { trackBranch(git, checkout, label); } else { // works for tags and local branches checkout.setName(label); } return checkout.call(); } protected boolean shouldPull(Git git) throws GitAPIException { boolean shouldPull; if (this.refreshRate > 0 && System.currentTimeMillis() - this.lastRefresh < (this.refreshRate * 1000)) { return false; } Status gitStatus; try { gitStatus = git.status().call(); } catch (JGitInternalException e) { onPullInvalidIndex(git, e); gitStatus = git.status().call(); } boolean isWorkingTreeClean = gitStatus.isClean(); String originUrl = git.getRepository().getConfig().getString("remote", "origin", "url"); if (this.forcePull && !isWorkingTreeClean) { shouldPull = true; logDirty(gitStatus); } else { shouldPull = isWorkingTreeClean && originUrl != null; } if (!isWorkingTreeClean && !this.forcePull) { this.logger.info("Cannot pull from remote " + originUrl + ", the working tree is not clean."); } return shouldPull; } protected void onPullInvalidIndex(Git git, JGitInternalException e) { if (!e.getMessage().contains("Short read of block.")) { throw e; } if (!this.forcePull) { throw e; } try { new File(getWorkingDirectory(), ".git/index").delete(); git.reset().setMode(ResetCommand.ResetType.HARD).setRef("HEAD").call(); } catch (GitAPIException ex) { e.addSuppressed(ex); throw e; } } @SuppressWarnings("unchecked") private void logDirty(Status status) { Set<String> dirties = dirties(status.getAdded(), status.getChanged(), status.getRemoved(), status.getMissing(), status.getModified(), status.getConflicting(), status.getUntracked() ); this.logger.warn(String.format("Dirty files found: %s", dirties)); } @SuppressWarnings("unchecked") private Set<String> dirties(Set<String>... changes) { Set<String> dirties = new HashSet<>(); for (Set<String> files : changes) { dirties.addAll(files); } return dirties; } private boolean shouldTrack(Git git, String label) throws GitAPIException { return isBranch(git, label) && !isLocalBranch(git, label); } protected FetchResult fetch(Git git, String label) { FetchCommand fetch = git.fetch(); fetch.setRemote("origin"); fetch.setTagOpt(TagOpt.FETCH_TAGS); fetch.setRemoveDeletedRefs(this.deleteUntrackedBranches); if (this.refreshRate > 0) { this.setLastRefresh(System.currentTimeMillis()); } configureCommand(fetch); try { FetchResult result = fetch.call(); if (result.getTrackingRefUpdates() != null && result.getTrackingRefUpdates().size() > 0) { this.logger.info("Fetched for remote " + label + " and found " + result.getTrackingRefUpdates().size() + " updates"); } return result; } catch (Exception ex) { String message = "Could not fetch remote for " + label + " remote: " + git.getRepository().getConfig().getString("remote", "origin", "url"); warn(message, ex); return null; } } private MergeResult merge(Git git, String label) { try { MergeCommand merge = git.merge(); merge.include(git.getRepository().findRef("origin/" + label)); MergeResult result = merge.call(); if (!result.getMergeStatus().isSuccessful()) { this.logger.warn("Merged from remote " + label + " with result " + result.getMergeStatus()); } return result; } catch (Exception ex) { String message = "Could not merge remote for " + label + " remote: " + git.getRepository().getConfig().getString("remote", "origin", "url"); warn(message, ex); return null; } } // private Ref resetHard(Git git, String label, String ref) { // ResetCommand reset = git.reset(); // reset.setRef(ref); // reset.setMode(ResetType.HARD); // try { // Ref resetRef = reset.call(); // if (resetRef != null) { // this.logger.info("Reset label " + label + " to version " + resetRef.getObjectId()); // } // return resetRef; // } catch (Exception ex) { // String message = "Could not reset to remote for " + label + " (current ref=" + ref + "), remote: " // + git.getRepository().getConfig().getString("remote", "origin", "url"); // warn(message, ex); // return null; // } // } private Git createGitClient() throws IOException, GitAPIException { File lock = new File(getWorkingDirectory(), ".git/index.lock"); if (lock.exists()) { // The only way this can happen is if another JVM (e.g. one that // crashed earlier) created the lock. We can attempt to recover by // wiping the slate clean. this.logger.info("Deleting stale JGit lock file at " + lock); lock.delete(); } if (new File(getWorkingDirectory(), ".git").exists()) { return openGitRepository(); } else { return copyRepository(); } } // Synchronize here so that multiple requests don't all try and delete the // base dir // together (this is a once only operation, so it only holds things up on // the first // request). private synchronized Git copyRepository() throws IOException, GitAPIException { deleteBaseDirIfExists(); getBasedir().mkdirs(); Assert.state(getBasedir().exists(), "Could not create basedir: " + getBasedir()); if (getUri().startsWith(FILE_URI_PREFIX)) { return copyFromLocalRepository(); } else { return cloneToBasedir(); } } private Git openGitRepository() throws IOException { Git git = this.gitFactory.getGitByOpen(getWorkingDirectory()); return git; } private Git copyFromLocalRepository() throws IOException { Git git; File remote = new UrlResource(StringUtils.cleanPath(getUri())).getFile(); Assert.state(remote.isDirectory(), "No directory at " + getUri()); File gitDir = new File(remote, ".git"); Assert.state(gitDir.exists(), "No .git at " + getUri()); Assert.state(gitDir.isDirectory(), "No .git directory at " + getUri()); git = this.gitFactory.getGitByOpen(remote); return git; } protected File getWorkingDirectory() { return this.basedir; } private Git cloneToBasedir() throws GitAPIException { CloneCommand clone = this.gitFactory.getCloneCommandByCloneRepository().setURI(getUri()) .setDirectory(getBasedir()); configureCommand(clone); try { return clone.call(); } catch (GitAPIException e) { this.logger.warn("Error occured cloning to base directory.", e); deleteBaseDirIfExists(); throw e; } } public File getBasedir() { return basedir; } private void deleteBaseDirIfExists() { if (getBasedir().exists()) { for (File file : getBasedir().listFiles()) { try { FileUtils.delete(file, FileUtils.RECURSIVE); } catch (IOException e) { throw new IllegalStateException("Failed to initialize base directory", e); } } } } protected File createBaseDir() { try { final Path basedir = Files.createTempDirectory("config-repo-"); Runtime.getRuntime().addShutdownHook(new Thread() { @Override public void run() { try { FileSystemUtils.deleteRecursively(basedir); } catch (IOException e) { logger.warn("Failed to delete temporary directory on exit: " + e); } } }); return basedir.toFile(); } catch (IOException e) { throw new IllegalStateException("Cannot create temp dir", e); } } private void initialize() { if (!this.initialized) { this.initialized = true; } } public String getUsername() { return username; } public String getPassword() { return password; } private void configureCommand(TransportCommand<?, ?> command) { command.setTimeout(this.timeout); if (this.transportConfigCallback != null) { command.setTransportConfigCallback(this.transportConfigCallback); } CredentialsProvider credentialsProvider = getCredentialsProvider(); if (credentialsProvider != null) { command.setCredentialsProvider(credentialsProvider); } } private CredentialsProvider getCredentialsProvider() { return this.gitCredentialsProviderFactory.createFor(this.getUri(), getUsername(), getPassword(), "", isSkipSslValidation() ); } private boolean isClean(Git git, String label) { StatusCommand status = git.status(); try { BranchTrackingStatus trackingStatus = BranchTrackingStatus.of(git.getRepository(), label); boolean isBranchAhead = trackingStatus != null && trackingStatus.getAheadCount() > 0; return status.call().isClean() && !isBranchAhead; } catch (Exception e) { String message = "Could not execute status command on local repository. Cause: (" + e.getClass().getSimpleName() + ") " + e.getMessage(); warn(message, e); return false; } } private void trackBranch(Git git, CheckoutCommand checkout, String label) { checkout.setCreateBranch(true).setName(label).setUpstreamMode(CreateBranchCommand.SetupUpstreamMode.TRACK) .setStartPoint("origin/" + label); } private boolean isBranch(Git git, String label) throws GitAPIException { return containsBranch(git, label, ListBranchCommand.ListMode.ALL); } private boolean isLocalBranch(Git git, String label) throws GitAPIException { return containsBranch(git, label, null); } private boolean containsBranch(Git git, String label, ListBranchCommand.ListMode listMode) throws GitAPIException { ListBranchCommand command = git.branchList(); if (listMode != null) { command.setListMode(listMode); } List<Ref> branches = command.call(); for (Ref ref : branches) { if (ref.getName().endsWith("/" + label)) { return true; } } return false; } protected void warn(String message, Exception ex) { this.logger.warn(message); if (this.logger.isDebugEnabled()) { this.logger.debug("Stacktrace for: " + message, ex); } } public long getLastRefresh() { return this.lastRefresh; } public void setLastRefresh(long lastRefresh) { this.lastRefresh = lastRefresh; } /** * Wraps the static method calls to {@link org.eclipse.jgit.api.Git} and {@link org.eclipse.jgit.api.CloneCommand} * allowing for easier unit testing. */ public static class JGitFactory { private final boolean cloneSubmodules; public JGitFactory() { this(false); } public JGitFactory(boolean cloneSubmodules) { this.cloneSubmodules = cloneSubmodules; } public Git getGitByOpen(File file) throws IOException { Git git = Git.open(file); return git; } public CloneCommand getCloneCommandByCloneRepository() { CloneCommand command = Git.cloneRepository().setCloneSubmodules(cloneSubmodules); return command; } } }