org.commonjava.aprox.subsys.git.GitManager Maven / Gradle / Ivy
The newest version!
/**
* Copyright (C) 2011 Red Hat, Inc. ([email protected])
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.commonjava.aprox.subsys.git;
import static org.apache.commons.lang.StringUtils.isEmpty;
import static org.apache.commons.lang.StringUtils.join;
import java.io.File;
import java.io.IOException;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import javax.enterprise.context.ApplicationScoped;
import org.commonjava.aprox.audit.ChangeSummary;
import org.commonjava.maven.atlas.ident.util.JoinString;
import org.eclipse.jgit.api.AddCommand;
import org.eclipse.jgit.api.CommitCommand;
import org.eclipse.jgit.api.Git;
import org.eclipse.jgit.api.RmCommand;
import org.eclipse.jgit.api.Status;
import org.eclipse.jgit.api.errors.GitAPIException;
import org.eclipse.jgit.api.errors.NoFilepatternException;
import org.eclipse.jgit.diff.DiffEntry;
import org.eclipse.jgit.diff.DiffFormatter;
import org.eclipse.jgit.errors.NoWorkTreeException;
import org.eclipse.jgit.errors.RevisionSyntaxException;
import org.eclipse.jgit.lib.Constants;
import org.eclipse.jgit.lib.IndexDiff.StageState;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.ObjectReader;
import org.eclipse.jgit.lib.PersonIdent;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.revplot.PlotCommit;
import org.eclipse.jgit.revplot.PlotCommitList;
import org.eclipse.jgit.revplot.PlotLane;
import org.eclipse.jgit.revplot.PlotWalk;
import org.eclipse.jgit.revwalk.RevCommit;
import org.eclipse.jgit.revwalk.RevTree;
import org.eclipse.jgit.revwalk.RevWalk;
import org.eclipse.jgit.storage.file.FileRepositoryBuilder;
import org.eclipse.jgit.treewalk.CanonicalTreeParser;
import org.eclipse.jgit.treewalk.FileTreeIterator;
import org.eclipse.jgit.treewalk.filter.AndTreeFilter;
import org.eclipse.jgit.treewalk.filter.PathFilter;
import org.eclipse.jgit.treewalk.filter.TreeFilter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@ApplicationScoped
public class GitManager
{
private final Logger logger = LoggerFactory.getLogger( getClass() );
private final Git git;
private final String email;
private final Repository repo;
private final File rootDir;
private final GitConfig config;
public GitManager( final GitConfig config )
throws GitSubsystemException
{
this.config = config;
rootDir = config.getContentDir();
final String cloneUrl = config.getCloneFrom();
boolean checkUpdate = false;
if ( cloneUrl != null )
{
logger.info( "Cloning: {} into: {}", cloneUrl, rootDir );
if ( rootDir.isDirectory() )
{
checkUpdate = true;
}
else
{
final boolean mkdirs = rootDir.mkdirs();
logger.info( "git dir {} (mkdir result: {}; is directory? {}) contains:\n {}", rootDir, mkdirs,
rootDir.isDirectory(), join( rootDir.listFiles(), "\n " ) );
try
{
Git.cloneRepository()
.setURI( cloneUrl )
.setDirectory( rootDir )
.setRemote( "origin" )
.call();
}
catch ( final GitAPIException e )
{
throw new GitSubsystemException( "Failed to clone remote URL: {} into: {}. Reason: {}", e,
cloneUrl, rootDir, e.getMessage() );
}
}
}
final File dotGitDir = new File( rootDir, ".git" );
logger.info( "Setting up git manager for: {}", dotGitDir );
try
{
repo = new FileRepositoryBuilder().readEnvironment()
.setGitDir( dotGitDir )
.build();
}
catch ( final IOException e )
{
throw new GitSubsystemException( "Failed to create Repository instance for: {}. Reason: {}", e, dotGitDir,
e.getMessage() );
}
String[] preExistingFromCreate = null;
if ( !dotGitDir.isDirectory() )
{
preExistingFromCreate = rootDir.list();
try
{
repo.create();
}
catch ( final IOException e )
{
throw new GitSubsystemException( "Failed to create git repo: {}. Reason: {}", e, rootDir,
e.getMessage() );
}
}
String originUrl = repo.getConfig()
.getString( "remote", "origin", "url" );
if ( originUrl == null )
{
originUrl = cloneUrl;
logger.info( "Setting origin URL: {}", originUrl );
repo.getConfig()
.setString( "remote", "origin", "url", originUrl );
repo.getConfig()
.setString( "remote", "origin", "fetch", "+refs/heads/*:refs/remotes/origin/*" );
}
String email = repo.getConfig()
.getString( "user", null, "email" );
if ( email == null )
{
email = config.getUserEmail();
}
if ( email == null )
{
try
{
email = "aprox@" + InetAddress.getLocalHost()
.getCanonicalHostName();
}
catch ( final UnknownHostException e )
{
throw new GitSubsystemException( "Failed to resolve 'localhost'. Reason: {}", e, e.getMessage() );
}
}
if ( repo.getConfig()
.getString( "user", null, "email" ) == null )
{
repo.getConfig()
.setString( "user", null, "email", email );
}
this.email = email;
git = new Git( repo );
if ( preExistingFromCreate != null && preExistingFromCreate.length > 0 )
{
addAndCommitPaths( new ChangeSummary( ChangeSummary.SYSTEM_USER, "Committing pre-existing files." ),
preExistingFromCreate );
}
if ( checkUpdate )
{
pullUpdates();
}
}
public GitManager addAndCommitFiles( final ChangeSummary summary, final File... files )
throws GitSubsystemException
{
return addAndCommitFiles( summary, Arrays.asList( files ) );
}
public GitManager addAndCommitFiles( final ChangeSummary summary, final Collection files )
throws GitSubsystemException
{
final Set paths = new HashSet<>();
for ( final File f : files )
{
final String path = relativize( f );
if ( path != null && path.length() > 0 )
{
paths.add( path );
}
}
return addAndCommitPaths( summary, paths );
}
private String relativize( final File f )
{
return Paths.get( rootDir.toURI() )
.relativize( Paths.get( f.toURI() ) )
.toString();
}
public GitManager addAndCommitPaths( final ChangeSummary summary, final String... paths )
throws GitSubsystemException
{
return addAndCommitPaths( summary, Arrays.asList( paths ) );
}
public GitManager addAndCommitPaths( final ChangeSummary summary, final Collection paths )
throws GitSubsystemException
{
if ( !verifyChangesExist( paths ) )
{
logger.info( "No actual changes in:\n {}\n\nSkipping commit.", join( paths, "\n " ) );
return this;
}
try
{
final AddCommand add = git.add();
final CommitCommand commit = git.commit();
for ( final String filepath : paths )
{
add.addFilepattern( filepath );
}
logger.info( "Adding:\n " + join( paths, "\n " ) + "\n\nSummary: " + summary );
add.call();
commit.setMessage( buildMessage( summary, paths ) )
.setAuthor( summary.getUser(), email )
.call();
}
catch ( final NoFilepatternException e )
{
throw new GitSubsystemException( "Cannot add to git: " + e.getMessage(), e );
}
catch ( final GitAPIException e )
{
throw new GitSubsystemException( "Cannot add to git: " + e.getMessage(), e );
}
return this;
}
private boolean verifyChangesExist( final Collection paths )
throws GitSubsystemException
{
try
{
final DiffFormatter formatter = new DiffFormatter( System.out );
formatter.setRepository( repo );
// resolve the HEAD object
final ObjectId oid = repo.resolve( Constants.HEAD );
if ( oid == null )
{
// if there's no head, then these must be real changes...
return true;
}
// reset a new tree object to the HEAD
final RevWalk walk = new RevWalk( repo );
final RevCommit commit = walk.parseCommit( oid );
final RevTree treeWalk = walk.parseTree( commit );
// construct filters for the paths we're trying to add/commit
final List filters = new ArrayList<>();
for ( final String path : paths )
{
filters.add( PathFilter.create( path ) );
}
// we're interested in trees with an actual diff. This should improve walk performance.
filters.add( TreeFilter.ANY_DIFF );
// set the path filters from above
walk.setTreeFilter( AndTreeFilter.create( filters ) );
// setup the tree for doing the comparison vs. uncommitted files
final CanonicalTreeParser tree = new CanonicalTreeParser();
final ObjectReader oldReader = repo.newObjectReader();
try
{
tree.reset( oldReader, treeWalk.getId() );
}
finally
{
oldReader.release();
}
walk.dispose();
// this iterator will actually scan the uncommitted files for diff'ing
final FileTreeIterator files = new FileTreeIterator( repo );
// do the scan.
final List entries = formatter.scan( tree, files );
// we're not interested in WHAT the differences are, only that there are differences.
return entries != null && !entries.isEmpty();
}
catch ( final IOException e )
{
throw new GitSubsystemException( "Failed to scan for actual changes among: %s. Reason: %s", e, paths,
e.getMessage() );
}
}
private String buildMessage( final ChangeSummary summary, final Collection paths )
{
final StringBuilder message = new StringBuilder().append( summary.getSummary() );
if ( config.isCommitFileManifestsEnabled() )
{
message.append( "\n\nFiles changed:\n" )
.append( join( paths, "\n" ) );
}
return message.toString();
}
public GitManager deleteAndCommit( final ChangeSummary summary, final File... deleted )
throws GitSubsystemException
{
return deleteAndCommit( summary, Arrays.asList( deleted ) );
}
public GitManager deleteAndCommit( final ChangeSummary summary, final Collection files )
throws GitSubsystemException
{
final Set paths = new HashSet<>();
for ( final File f : files )
{
final String path = relativize( f );
if ( path != null && path.length() > 0 )
{
paths.add( path );
}
}
return deleteAndCommitPaths( summary, paths );
}
public GitManager deleteAndCommitPaths( final ChangeSummary summary, final String... paths )
throws GitSubsystemException
{
return deleteAndCommitPaths( summary, Arrays.asList( paths ) );
}
public GitManager deleteAndCommitPaths( final ChangeSummary summary, final Collection paths )
throws GitSubsystemException
{
try
{
RmCommand rm = git.rm();
CommitCommand commit = git.commit();
for ( final String path : paths )
{
rm = rm.addFilepattern( path );
commit = commit.setOnly( path );
}
logger.info( "Deleting:\n " + join( paths, "\n " ) + "\n\nSummary: " + summary );
rm.call();
commit.setMessage( buildMessage( summary, paths ) )
.setAuthor( summary.getUser(), email )
.call();
}
catch ( final NoFilepatternException e )
{
throw new GitSubsystemException( "Cannot remove from git: " + e.getMessage(), e );
}
catch ( final GitAPIException e )
{
throw new GitSubsystemException( "Cannot remove from git: " + e.getMessage(), e );
}
return this;
}
public ChangeSummary getHeadCommit( final File f )
throws GitSubsystemException
{
try
{
final ObjectId oid = repo.resolve( "HEAD" );
final PlotWalk pw = new PlotWalk( repo );
final RevCommit rc = pw.parseCommit( oid );
pw.markStart( rc );
final String filepath = relativize( f );
pw.setTreeFilter( AndTreeFilter.create( PathFilter.create( filepath ), TreeFilter.ANY_DIFF ) );
final PlotCommitList cl = new PlotCommitList<>();
cl.source( pw );
cl.fillTo( 1 );
final PlotCommit commit = cl.get( 0 );
return toChangeSummary( commit );
}
catch ( RevisionSyntaxException | IOException e )
{
throw new GitSubsystemException( "Failed to resolve HEAD commit for: %s. Reason: %s", e, f, e.getMessage() );
}
}
public List getChangelog( final File f, final int start, final int length )
throws GitSubsystemException
{
if ( length == 0 )
{
return Collections.emptyList();
}
try
{
final ObjectId oid = repo.resolve( Constants.HEAD );
final PlotWalk pw = new PlotWalk( repo );
final RevCommit rc = pw.parseCommit( oid );
toChangeSummary( rc );
pw.markStart( rc );
final String filepath = relativize( f );
logger.info( "Getting changelog for: {} (start: {}, length: {})", filepath, start, length );
if ( !isEmpty( filepath ) && !filepath.equals( "/" ) )
{
pw.setTreeFilter( AndTreeFilter.create( PathFilter.create( filepath ), TreeFilter.ANY_DIFF ) );
}
else
{
pw.setTreeFilter( TreeFilter.ANY_DIFF );
}
final List changelogs = new ArrayList();
int count = 0;
final int stop = length > 0 ? length + 1 : 0;
RevCommit commit = null;
while ( ( commit = pw.next() ) != null && ( stop < 1 || count < stop ) )
{
if ( count < start )
{
count++;
continue;
}
// printFiles( commit );
changelogs.add( toChangeSummary( commit ) );
count++;
}
if ( length < -1 )
{
final int remove = ( -1 * length ) - 1;
for ( int i = 0; i < remove; i++ )
{
changelogs.remove( changelogs.size() - 1 );
}
}
return changelogs;
}
catch ( RevisionSyntaxException | IOException e )
{
throw new GitSubsystemException( "Failed to resolve HEAD commit for: %s. Reason: %s", e, f, e.getMessage() );
}
}
// private void printFiles( final RevCommit commit )
// throws IOException
// {
// final RevWalk tree = new RevWalk( repo );
// final RevCommit parent = commit.getParentCount() > 0 ? tree.parseCommit( commit.getParent( 0 )
// .getId() ) : null;
//
// final DiffFormatter df = new DiffFormatter( DisabledOutputStream.INSTANCE );
// df.setRepository( repo );
// df.setDiffComparator( RawTextComparator.DEFAULT );
// df.setDetectRenames( true );
//
// final List diffs;
// if ( parent == null )
// {
// diffs =
// df.scan( new EmptyTreeIterator(),
// new CanonicalTreeParser( null, tree.getObjectReader(), commit.getTree() ) );
// }
// else
// {
// diffs = df.scan( parent.getTree(), commit.getTree() );
// }
//
// for ( final DiffEntry diff : diffs )
// {
// logger.info( "({} {} {}", diff.getChangeType()
// .name(), diff.getNewMode()
// .getBits(), diff.getNewPath() );
// }
// }
private ChangeSummary toChangeSummary( final RevCommit commit )
{
final PersonIdent who = commit.getAuthorIdent();
final Date when = new Date( TimeUnit.MILLISECONDS.convert( commit.getCommitTime(), TimeUnit.SECONDS ) );
return new ChangeSummary( who.getName(), commit.getFullMessage(), when, commit.getId()
.name() );
}
public GitManager pullUpdates()
throws GitSubsystemException
{
return pullUpdates( ConflictStrategy.merge );
}
public GitManager pullUpdates( final ConflictStrategy strategy )
throws GitSubsystemException
{
try
{
git.pull()
.setStrategy( strategy.mergeStrategy() )
.setRemoteBranchName( config.getRemoteBranchName() )
.setRebase( true )
.call();
}
catch ( final GitAPIException e )
{
throw new GitSubsystemException( "Cannot pull content updates via git: " + e.getMessage(), e );
}
return this;
}
public GitManager pushUpdates()
throws GitSubsystemException
{
try
{
git.push()
.call();
}
catch ( final GitAPIException e )
{
throw new GitSubsystemException( "Cannot push content updates via git: " + e.getMessage(), e );
}
return this;
}
public String getOriginUrl()
{
return git.getRepository()
.getConfig()
.getString( "remote", "origin", "url" );
}
public GitManager commitModifiedFiles( final ChangeSummary changeSummary )
throws GitSubsystemException
{
Status status;
try
{
status = git.status()
.call();
}
catch ( NoWorkTreeException | GitAPIException e )
{
throw new GitSubsystemException( "Failed to retrieve status of: %s. Reason: %s", e, rootDir, e.getMessage() );
}
final Map css = status.getConflictingStageState();
if ( !css.isEmpty() )
{
throw new GitSubsystemException( "%s contains conflicts. Cannot auto-commit.\n %s", rootDir,
new JoinString( "\n ", css.entrySet() ) );
}
final Set toAdd = new HashSet<>();
final Set modified = status.getModified();
if ( modified != null && !modified.isEmpty() )
{
toAdd.addAll( modified );
}
final Set untracked = status.getUntracked();
if ( untracked != null && !untracked.isEmpty() )
{
toAdd.addAll( untracked );
}
final Set untrackedFolders = status.getUntrackedFolders();
if ( untrackedFolders != null && !untrackedFolders.isEmpty() )
{
toAdd.addAll( untrackedFolders );
// for ( String folderPath : untrackedFolders )
// {
// File dir = new File( rootDir, folderPath );
// Files.walkFileTree( null, null )
// }
}
if ( !toAdd.isEmpty() )
{
addAndCommitPaths( changeSummary, toAdd );
}
return this;
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy